plugin.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  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. ***************************************************************************/
  9. /***************************************************************************
  10. * *
  11. * This program is free software; you can redistribute it and/or modify *
  12. * it under the terms of the GNU General Public License as published by *
  13. * the Free Software Foundation; either version 2 of the License, or *
  14. * (at your option) any later version. *
  15. * *
  16. ***************************************************************************/
  17. """
  18. # this will disable the dbplugin if the connector raise an ImportError
  19. from .connector import PostGisDBConnector
  20. from qgis.PyQt.QtCore import Qt, QRegExp, QCoreApplication
  21. from qgis.PyQt.QtGui import QIcon
  22. from qgis.PyQt.QtWidgets import QAction, QApplication, QMessageBox
  23. from qgis.core import Qgis, QgsApplication, QgsSettings
  24. from qgis.gui import QgsMessageBar
  25. from ..plugin import ConnectionError, InvalidDataException, DBPlugin, Database, Schema, Table, VectorTable, RasterTable, \
  26. TableField, TableConstraint, TableIndex, TableTrigger, TableRule
  27. import re
  28. def classFactory():
  29. return PostGisDBPlugin
  30. class PostGisDBPlugin(DBPlugin):
  31. @classmethod
  32. def icon(self):
  33. return QgsApplication.getThemeIcon("/mIconPostgis.svg")
  34. @classmethod
  35. def typeName(self):
  36. return 'postgis'
  37. @classmethod
  38. def typeNameString(self):
  39. return QCoreApplication.translate('db_manager', 'PostGIS')
  40. @classmethod
  41. def providerName(self):
  42. return 'postgres'
  43. @classmethod
  44. def connectionSettingsKey(self):
  45. return '/PostgreSQL/connections'
  46. def databasesFactory(self, connection, uri):
  47. return PGDatabase(connection, uri)
  48. def connect(self, parent=None):
  49. conn_name = self.connectionName()
  50. settings = QgsSettings()
  51. settings.beginGroup("/%s/%s" % (self.connectionSettingsKey(), conn_name))
  52. if not settings.contains("database"): # non-existent entry?
  53. raise InvalidDataException(self.tr('There is no defined database connection "{0}".').format(conn_name))
  54. from qgis.core import QgsDataSourceUri
  55. uri = QgsDataSourceUri()
  56. settingsList = ["service", "host", "port", "database", "username", "password", "authcfg"]
  57. service, host, port, database, username, password, authcfg = (settings.value(x, "", type=str) for x in settingsList)
  58. useEstimatedMetadata = settings.value("estimatedMetadata", False, type=bool)
  59. try:
  60. sslmode = settings.enumValue("sslmode", QgsDataSourceUri.SslPrefer)
  61. except TypeError:
  62. sslmode = QgsDataSourceUri.SslPrefer
  63. settings.endGroup()
  64. if hasattr(authcfg, 'isNull') and authcfg.isNull():
  65. authcfg = ''
  66. if service:
  67. uri.setConnection(service, database, username, password, sslmode, authcfg)
  68. else:
  69. uri.setConnection(host, port, database, username, password, sslmode, authcfg)
  70. uri.setUseEstimatedMetadata(useEstimatedMetadata)
  71. try:
  72. return self.connectToUri(uri)
  73. except ConnectionError:
  74. return False
  75. class PGDatabase(Database):
  76. def __init__(self, connection, uri):
  77. Database.__init__(self, connection, uri)
  78. def connectorsFactory(self, uri):
  79. return PostGisDBConnector(uri, self.connection())
  80. def dataTablesFactory(self, row, db, schema=None):
  81. return PGTable(row, db, schema)
  82. def info(self):
  83. from .info_model import PGDatabaseInfo
  84. return PGDatabaseInfo(self)
  85. def vectorTablesFactory(self, row, db, schema=None):
  86. return PGVectorTable(row, db, schema)
  87. def rasterTablesFactory(self, row, db, schema=None):
  88. return PGRasterTable(row, db, schema)
  89. def schemasFactory(self, row, db):
  90. return PGSchema(row, db)
  91. def sqlResultModel(self, sql, parent):
  92. from .data_model import PGSqlResultModel
  93. return PGSqlResultModel(self, sql, parent)
  94. def sqlResultModelAsync(self, sql, parent):
  95. from .data_model import PGSqlResultModelAsync
  96. return PGSqlResultModelAsync(self, sql, parent)
  97. def registerDatabaseActions(self, mainWindow):
  98. Database.registerDatabaseActions(self, mainWindow)
  99. # add a separator
  100. separator = QAction(self)
  101. separator.setSeparator(True)
  102. mainWindow.registerAction(separator, self.tr("&Table"))
  103. action = QAction(self.tr("Run &Vacuum Analyze"), self)
  104. mainWindow.registerAction(action, self.tr("&Table"), self.runVacuumAnalyzeActionSlot)
  105. action = QAction(self.tr("Run &Refresh Materialized View"), self)
  106. mainWindow.registerAction(action, self.tr("&Table"), self.runRefreshMaterializedViewSlot)
  107. def runVacuumAnalyzeActionSlot(self, item, action, parent):
  108. QApplication.restoreOverrideCursor()
  109. try:
  110. if not isinstance(item, Table) or item.isView:
  111. parent.infoBar.pushMessage(self.tr("Select a table for vacuum analyze."), Qgis.Info,
  112. parent.iface.messageTimeout())
  113. return
  114. finally:
  115. QApplication.setOverrideCursor(Qt.WaitCursor)
  116. item.runVacuumAnalyze()
  117. def runRefreshMaterializedViewSlot(self, item, action, parent):
  118. QApplication.restoreOverrideCursor()
  119. try:
  120. if not isinstance(item, PGTable) or item._relationType != 'm':
  121. parent.infoBar.pushMessage(self.tr("Select a materialized view for refresh."), Qgis.Info,
  122. parent.iface.messageTimeout())
  123. return
  124. finally:
  125. QApplication.setOverrideCursor(Qt.WaitCursor)
  126. item.runRefreshMaterializedView()
  127. def hasLowercaseFieldNamesOption(self):
  128. return True
  129. def supportsComment(self):
  130. return True
  131. def executeSql(self, sql):
  132. return self.connector._executeSql(sql)
  133. class PGSchema(Schema):
  134. def __init__(self, row, db):
  135. Schema.__init__(self, db)
  136. self.oid, self.name, self.owner, self.perms, self.comment = row
  137. class PGTable(Table):
  138. def __init__(self, row, db, schema=None):
  139. Table.__init__(self, db, schema)
  140. self.name, schema_name, self._relationType, self.owner, self.estimatedRowCount, self.pages, self.comment = row
  141. self.isView = self._relationType in {'v', 'm'}
  142. self.estimatedRowCount = int(self.estimatedRowCount)
  143. def runVacuumAnalyze(self):
  144. self.aboutToChange.emit()
  145. self.database().connector.runVacuumAnalyze((self.schemaName(), self.name))
  146. # TODO: change only this item, not re-create all the tables in the schema/database
  147. self.schema().refresh() if self.schema() else self.database().refresh()
  148. def runRefreshMaterializedView(self):
  149. self.aboutToChange.emit()
  150. self.database().connector.runRefreshMaterializedView((self.schemaName(), self.name))
  151. # TODO: change only this item, not re-create all the tables in the schema/database
  152. self.schema().refresh() if self.schema() else self.database().refresh()
  153. def runAction(self, action):
  154. action = str(action)
  155. if action.startswith("vacuumanalyze/"):
  156. if action == "vacuumanalyze/run":
  157. self.runVacuumAnalyze()
  158. return True
  159. elif action.startswith("rule/"):
  160. parts = action.split('/')
  161. rule_name = parts[1]
  162. rule_action = parts[2]
  163. msg = self.tr("Do you want to {0} rule {1}?").format(rule_action, rule_name)
  164. QApplication.restoreOverrideCursor()
  165. try:
  166. if QMessageBox.question(None, self.tr("Table rule"), msg,
  167. QMessageBox.Yes | QMessageBox.No) == QMessageBox.No:
  168. return False
  169. finally:
  170. QApplication.setOverrideCursor(Qt.WaitCursor)
  171. if rule_action == "delete":
  172. self.aboutToChange.emit()
  173. self.database().connector.deleteTableRule(rule_name, (self.schemaName(), self.name))
  174. self.refreshRules()
  175. return True
  176. elif action.startswith("refreshmaterializedview/"):
  177. if action == "refreshmaterializedview/run":
  178. self.runRefreshMaterializedView()
  179. return True
  180. return Table.runAction(self, action)
  181. def tableFieldsFactory(self, row, table):
  182. return PGTableField(row, table)
  183. def tableConstraintsFactory(self, row, table):
  184. return PGTableConstraint(row, table)
  185. def tableIndexesFactory(self, row, table):
  186. return PGTableIndex(row, table)
  187. def tableTriggersFactory(self, row, table):
  188. return PGTableTrigger(row, table)
  189. def tableRulesFactory(self, row, table):
  190. return PGTableRule(row, table)
  191. def info(self):
  192. from .info_model import PGTableInfo
  193. return PGTableInfo(self)
  194. def crs(self):
  195. return self.database().connector.getCrs(self.srid)
  196. def tableDataModel(self, parent):
  197. from .data_model import PGTableDataModel
  198. return PGTableDataModel(self, parent)
  199. def delete(self):
  200. self.aboutToChange.emit()
  201. if self.isView:
  202. ret = self.database().connector.deleteView((self.schemaName(), self.name), self._relationType == 'm')
  203. else:
  204. ret = self.database().connector.deleteTable((self.schemaName(), self.name))
  205. if not ret:
  206. self.deleted.emit()
  207. return ret
  208. class PGVectorTable(PGTable, VectorTable):
  209. def __init__(self, row, db, schema=None):
  210. PGTable.__init__(self, row[:-4], db, schema)
  211. VectorTable.__init__(self, db, schema)
  212. self.geomColumn, self.geomType, self.geomDim, self.srid = row[-4:]
  213. def info(self):
  214. from .info_model import PGVectorTableInfo
  215. return PGVectorTableInfo(self)
  216. def runAction(self, action):
  217. if PGTable.runAction(self, action):
  218. return True
  219. return VectorTable.runAction(self, action)
  220. def geometryType(self):
  221. """ Returns the proper WKT type.
  222. PostGIS records type like this:
  223. | WKT Type | geomType | geomDim |
  224. |--------------|-------------|---------|
  225. | LineString | LineString | 2 |
  226. | LineStringZ | LineString | 3 |
  227. | LineStringM | LineStringM | 3 |
  228. | LineStringZM | LineString | 4 |
  229. """
  230. geometryType = self.geomType
  231. if self.geomDim == 3 and self.geomType[-1] != "M":
  232. geometryType += "Z"
  233. elif self.geomDim == 4:
  234. geometryType += "ZM"
  235. return geometryType
  236. class PGRasterTable(PGTable, RasterTable):
  237. def __init__(self, row, db, schema=None):
  238. PGTable.__init__(self, row[:-6], db, schema)
  239. RasterTable.__init__(self, db, schema)
  240. self.geomColumn, self.pixelType, self.pixelSizeX, self.pixelSizeY, self.isExternal, self.srid = row[-6:]
  241. self.geomType = 'RASTER'
  242. def info(self):
  243. from .info_model import PGRasterTableInfo
  244. return PGRasterTableInfo(self)
  245. def uri(self, uri=None):
  246. """Returns the datasource URI for postgresraster provider"""
  247. if not uri:
  248. uri = self.database().uri()
  249. service = ('service=\'%s\'' % uri.service()) if uri.service() else ''
  250. dbname = ('dbname=\'%s\'' % uri.database()) if uri.database() else ''
  251. host = ('host=%s' % uri.host()) if uri.host() else ''
  252. user = ('user=%s' % uri.username()) if uri.username() else ''
  253. passw = ('password=%s' % uri.password()) if uri.password() else ''
  254. port = ('port=%s' % uri.port()) if uri.port() else ''
  255. schema = self.schemaName() if self.schemaName() else 'public'
  256. table = '"%s"."%s"' % (schema, self.name)
  257. if not dbname:
  258. # postgresraster provider *requires* a dbname
  259. connector = self.database().connector
  260. r = connector._execute(None, "SELECT current_database()")
  261. dbname = ('dbname=\'%s\'' % connector._fetchone(r)[0])
  262. connector._close_cursor(r)
  263. # Find first raster field
  264. col = ''
  265. for fld in self.fields():
  266. if fld.dataType == "raster":
  267. col = 'column=\'%s\'' % fld.name
  268. break
  269. uri = '%s %s %s %s %s %s %s table=%s' % \
  270. (service, dbname, host, user, passw, port, col, table)
  271. return uri
  272. def mimeUri(self):
  273. uri = "raster:postgresraster:{}:{}".format(self.name, re.sub(":", r"\:", self.uri()))
  274. return uri
  275. def toMapLayer(self, geometryType=None, crs=None):
  276. from qgis.core import QgsRasterLayer, QgsContrastEnhancement, QgsDataSourceUri, QgsCredentials
  277. rl = QgsRasterLayer(self.uri(), self.name, "postgresraster")
  278. if not rl.isValid():
  279. err = rl.error().summary()
  280. uri = QgsDataSourceUri(self.database().uri())
  281. conninfo = uri.connectionInfo(False)
  282. username = uri.username()
  283. password = uri.password()
  284. for i in range(3):
  285. (ok, username, password) = QgsCredentials.instance().get(conninfo, username, password, err)
  286. if ok:
  287. uri.setUsername(username)
  288. uri.setPassword(password)
  289. rl = QgsRasterLayer(self.uri(uri), self.name)
  290. if rl.isValid():
  291. break
  292. if rl.isValid():
  293. rl.setContrastEnhancement(QgsContrastEnhancement.StretchToMinimumMaximum)
  294. return rl
  295. class PGTableField(TableField):
  296. def __init__(self, row, table):
  297. TableField.__init__(self, table)
  298. self.num, self.name, self.dataType, self.charMaxLen, self.modifier, self.notNull, self.hasDefault, self.default, typeStr = row
  299. self.primaryKey = False
  300. # get modifier (e.g. "precision,scale") from formatted type string
  301. trimmedTypeStr = typeStr.strip()
  302. regex = QRegExp("\\((.+)\\)$")
  303. startpos = regex.indexIn(trimmedTypeStr)
  304. if startpos >= 0:
  305. self.modifier = regex.cap(1).strip()
  306. else:
  307. self.modifier = None
  308. # find out whether fields are part of primary key
  309. for con in self.table().constraints():
  310. if con.type == TableConstraint.TypePrimaryKey and self.num in con.columns:
  311. self.primaryKey = True
  312. break
  313. def getComment(self):
  314. """Returns the comment for a field"""
  315. tab = self.table()
  316. # SQL Query checking if a comment exists for the field
  317. sql_cpt = "Select count(*) from pg_description pd, pg_class pc, pg_attribute pa where relname = '%s' and attname = '%s' and pa.attrelid = pc.oid and pd.objoid = pc.oid and pd.objsubid = pa.attnum" % (tab.name, self.name)
  318. # SQL Query that return the comment of the field
  319. sql = "Select pd.description from pg_description pd, pg_class pc, pg_attribute pa where relname = '%s' and attname = '%s' and pa.attrelid = pc.oid and pd.objoid = pc.oid and pd.objsubid = pa.attnum" % (tab.name, self.name)
  320. c = tab.database().connector._execute(None, sql_cpt) # Execute Check query
  321. res = tab.database().connector._fetchone(c)[0] # Store result
  322. if res == 1:
  323. # When a comment exists
  324. c = tab.database().connector._execute(None, sql) # Execute query
  325. res = tab.database().connector._fetchone(c)[0] # Store result
  326. tab.database().connector._close_cursor(c) # Close cursor
  327. return res # Return comment
  328. else:
  329. return ''
  330. class PGTableConstraint(TableConstraint):
  331. def __init__(self, row, table):
  332. TableConstraint.__init__(self, table)
  333. self.name, constr_type_str, self.isDefferable, self.isDeffered, columns = row[:5]
  334. self.columns = list(map(int, columns.split(' ')))
  335. if constr_type_str in TableConstraint.types:
  336. self.type = TableConstraint.types[constr_type_str]
  337. else:
  338. self.type = TableConstraint.TypeUnknown
  339. if self.type == TableConstraint.TypeCheck:
  340. self.checkSource = row[5]
  341. elif self.type == TableConstraint.TypeForeignKey:
  342. self.foreignTable = row[6]
  343. self.foreignOnUpdate = TableConstraint.onAction[row[7]]
  344. self.foreignOnDelete = TableConstraint.onAction[row[8]]
  345. self.foreignMatchType = TableConstraint.matchTypes[row[9]]
  346. self.foreignKeys = row[10]
  347. class PGTableIndex(TableIndex):
  348. def __init__(self, row, table):
  349. TableIndex.__init__(self, table)
  350. self.name, columns, self.isUnique = row
  351. self.columns = list(map(int, columns.split(' ')))
  352. class PGTableTrigger(TableTrigger):
  353. def __init__(self, row, table):
  354. TableTrigger.__init__(self, table)
  355. self.name, self.function, self.type, self.enabled = row
  356. class PGTableRule(TableRule):
  357. def __init__(self, row, table):
  358. TableRule.__init__(self, table)
  359. self.name, self.definition = row