From ce4fc1daa5b49d053790b258d1361a2203282453 Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Thu, 29 Dec 2011 00:02:47 +0100 Subject: [PATCH] [FEATURE][API] postgres provider refactoring: - use m prefix for member variables and s prefix for class variables - refactor QgsPostgresProvider::Conn and QgsPostgresConnection to QgsPostgresConn - put QgsColumnTypeThread into separate file - use QgsPostgresConn in QgsPgSourceSelect - cleaner abort of column type thread (e.g. remove unfinished columns from selection list) - [API] move QgsDbTableModel as QgsPgTableModel to provider - [FEATURE] support for arbitrary key (including non-numeric and multi column) - [FEATURE][API] support for requesting a certain geometry type and/or srid in QgsDataSourceURI * no more explicit filter strings * support for empty tables without gemetry_columns entry or GEOMETRY type. --- src/app/legend/qgslegendlayer.cpp | 3 - src/core/CMakeLists.txt | 2 - src/core/qgis.h | 2 +- src/core/qgsdatasourceuri.cpp | 68 + src/core/qgsdatasourceuri.h | 16 +- src/providers/postgres/CMakeLists.txt | 8 +- .../postgres/qgscolumntypethread.cpp | 64 + src/providers/postgres/qgscolumntypethread.h | 53 + .../postgres/qgsdbfilterproxymodel.cpp | 53 + .../postgres/qgsdbfilterproxymodel.h | 39 + src/providers/postgres/qgspgnewconnection.cpp | 60 +- src/providers/postgres/qgspgsourceselect.cpp | 352 +- src/providers/postgres/qgspgsourceselect.h | 111 +- .../postgres/qgspgtablemodel.cpp} | 227 +- .../postgres/qgspgtablemodel.h} | 28 +- src/providers/postgres/qgspostgresconn.cpp | 1228 +++ src/providers/postgres/qgspostgresconn.h | 236 + .../postgres/qgspostgresconnection.cpp | 132 - .../postgres/qgspostgresconnection.h | 51 - .../postgres/qgspostgresdataitems.cpp | 212 +- src/providers/postgres/qgspostgresdataitems.h | 105 +- .../postgres/qgspostgresprovider.cpp | 6782 +++++++---------- src/providers/postgres/qgspostgresprovider.h | 399 +- .../testdata}/create_test_tables | 0 24 files changed, 5043 insertions(+), 5188 deletions(-) create mode 100644 src/providers/postgres/qgscolumntypethread.cpp create mode 100644 src/providers/postgres/qgscolumntypethread.h create mode 100644 src/providers/postgres/qgsdbfilterproxymodel.cpp create mode 100644 src/providers/postgres/qgsdbfilterproxymodel.h rename src/{core/qgsdbtablemodel.cpp => providers/postgres/qgspgtablemodel.cpp} (50%) rename src/{core/qgsdbtablemodel.h => providers/postgres/qgspgtablemodel.h} (75%) create mode 100644 src/providers/postgres/qgspostgresconn.cpp create mode 100644 src/providers/postgres/qgspostgresconn.h delete mode 100644 src/providers/postgres/qgspostgresconnection.cpp delete mode 100644 src/providers/postgres/qgspostgresconnection.h rename {src/providers/postgres => tests/testdata}/create_test_tables (100%) diff --git a/src/app/legend/qgslegendlayer.cpp b/src/app/legend/qgslegendlayer.cpp index 1941f07f265..8bc32fe7e69 100644 --- a/src/app/legend/qgslegendlayer.cpp +++ b/src/app/legend/qgslegendlayer.cpp @@ -378,13 +378,10 @@ QPixmap QgsLegendLayer::getOriginalPixmap() { case QGis::Point: return QgisApp::getThemePixmap( "/mIconPointLayer.png" ); - break; case QGis::Line: return QgisApp::getThemePixmap( "/mIconLineLayer.png" ); - break; case QGis::Polygon: return QgisApp::getThemePixmap( "/mIconPolygonLayer.png" ); - break; case QGis::NoGeometry: return QgisApp::getThemePixmap( "/mIconTableLayer.png" ); default: diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 0b53b60bfa8..30add7cb6e6 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -54,7 +54,6 @@ SET(QGIS_CORE_SRCS qgsdatasourceuri.cpp qgsdataitem.cpp qgsdbfilterproxymodel.cpp - qgsdbtablemodel.cpp qgsdiagram.cpp qgsdiagramrendererv2.cpp qgsdistancearea.cpp @@ -235,7 +234,6 @@ SET(QGIS_CORE_MOC_HDRS qgscontexthelp.h qgscoordinatetransform.h qgsdataitem.h - qgsdbtablemodel.h qgsdataprovider.h qgshttptransaction.h qgsmaplayer.h diff --git a/src/core/qgis.h b/src/core/qgis.h index 44daf494a93..0ae6fd79827 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -90,7 +90,7 @@ class CORE_EXPORT QGis DegreesMinutesSeconds = 4, DegreesDecimalMinutes = 5, UnknownUnit = 3 - } ; + }; //! User defined event types enum UserEvent diff --git a/src/core/qgsdatasourceuri.cpp b/src/core/qgsdatasourceuri.cpp index 12261d3e005..a8f4d65956f 100644 --- a/src/core/qgsdatasourceuri.cpp +++ b/src/core/qgsdatasourceuri.cpp @@ -27,6 +27,7 @@ QgsDataSourceURI::QgsDataSourceURI() , mKeyColumn( "" ) , mUseEstimatedMetadata( false ) , mSelectAtIdDisabled( false ) + , mGeometryType( QGis::WKBUnknown ) { // do nothing } @@ -36,6 +37,7 @@ QgsDataSourceURI::QgsDataSourceURI( QString uri ) , mKeyColumn( "" ) , mUseEstimatedMetadata( false ) , mSelectAtIdDisabled( false ) + , mGeometryType( QGis::WKBUnknown ) { int i = 0; while ( i < uri.length() ) @@ -123,6 +125,42 @@ QgsDataSourceURI::QgsDataSourceURI( QString uri ) { mUseEstimatedMetadata = pval == "true"; } + else if ( pname == "srid" ) + { + mSrid = pval; + } + else if ( pname == "type" ) + { + QString geomTypeUpper = pval.toUpper(); + if ( geomTypeUpper == "POINT" ) + { + mGeometryType = QGis::WKBPoint; + } + else if ( geomTypeUpper == "MULTIPOINT" ) + { + mGeometryType = QGis::WKBMultiPoint; + } + else if ( geomTypeUpper == "LINESTRING" ) + { + mGeometryType = QGis::WKBLineString; + } + else if ( geomTypeUpper == "MULTILINESTRING" ) + { + mGeometryType = QGis::WKBMultiLineString; + } + else if ( geomTypeUpper == "POLYGON" ) + { + mGeometryType = QGis::WKBPolygon; + } + else if ( geomTypeUpper == "MULTIPOLYGON" ) + { + mGeometryType = QGis::WKBMultiPolygon; + } + else + { + mGeometryType = QGis::WKBUnknown; + } + } else if ( pname == "selectatid" ) { mSelectAtIdDisabled = pval == "false"; @@ -483,6 +521,16 @@ QString QgsDataSourceURI::uri() const theUri += QString( " estimatedmetadata=true" ); } + if ( !mSrid.isEmpty() ) + { + theUri += QString( " srid=%1" ).arg( mSrid ); + } + + if ( mGeometryType != QGis::WKBUnknown && mGeometryType != QGis::WKBNoGeometry ) + { + theUri += QString( " type=%1" ).arg( QGis::qgisFeatureTypes[mGeometryType] + 3 ); + } + if ( mSelectAtIdDisabled ) { theUri += QString( " selectatid=false" ); @@ -552,3 +600,23 @@ void QgsDataSourceURI::setDatabase( const QString &database ) { mDatabase = database; } + +QGis::WkbType QgsDataSourceURI::geometryType() const +{ + return mGeometryType; +} + +void QgsDataSourceURI::setGeometryType( QGis::WkbType geometryType ) +{ + mGeometryType = geometryType; +} + +QString QgsDataSourceURI::srid() const +{ + return mSrid; +} + +void QgsDataSourceURI::setSrid( QString srid ) +{ + mSrid = srid; +} diff --git a/src/core/qgsdatasourceuri.h b/src/core/qgsdatasourceuri.h index 5887d1a27ce..ae3517f95a7 100644 --- a/src/core/qgsdatasourceuri.h +++ b/src/core/qgsdatasourceuri.h @@ -19,7 +19,7 @@ #ifndef QGSDATASOURCEURI_H #define QGSDATASOURCEURI_H -#include +#include "qgis.h" /** \ingroup core * Class for storing the component parts of a PostgreSQL/RDBMS datasource URI. @@ -117,6 +117,12 @@ class CORE_EXPORT QgsDataSourceURI QString keyColumn() const; void setKeyColumn( QString column ); + // added in 1.9 + QGis::WkbType geometryType() const; + void setGeometryType( QGis::WkbType type ); + QString srid() const; + void setSrid( QString srid ); + private: void skipBlanks( const QString &uri, int &i ); QString getValue( const QString &uri, int &i ); @@ -148,10 +154,14 @@ class CORE_EXPORT QgsDataSourceURI enum SSLmode mSSLmode; //! key column QString mKeyColumn; - //Use estimated metadata flag + //! Use estimated metadata flag bool mUseEstimatedMetadata; - //Disable SelectAtId capability (eg. to trigger the attribute table memory model for expensive views) + //! Disable SelectAtId capability (eg. to trigger the attribute table memory model for expensive views) bool mSelectAtIdDisabled; + //! geometry type (or QGis::WKBUnknown if not specified) + QGis::WkbType mGeometryType; + //! SRID or a null string if not specified + QString mSrid; }; #endif //QGSDATASOURCEURI_H diff --git a/src/providers/postgres/CMakeLists.txt b/src/providers/postgres/CMakeLists.txt index 4d05339a129..350dcf9acaa 100644 --- a/src/providers/postgres/CMakeLists.txt +++ b/src/providers/postgres/CMakeLists.txt @@ -4,17 +4,21 @@ SET(PG_SRCS qgspostgresprovider.cpp - qgspostgresconnection.cpp + qgspostgresconn.cpp qgspostgresdataitems.cpp qgspgsourceselect.cpp qgspgnewconnection.cpp + qgspgtablemodel.cpp + qgscolumntypethread.cpp ) SET(PG_MOC_HDRS qgspostgresprovider.h - qgspostgresconnection.h + qgspostgresconn.h qgspostgresdataitems.h qgspgsourceselect.h qgspgnewconnection.h + qgspgtablemodel.h + qgscolumntypethread.h ) diff --git a/src/providers/postgres/qgscolumntypethread.cpp b/src/providers/postgres/qgscolumntypethread.cpp new file mode 100644 index 00000000000..d64d5fe2bb4 --- /dev/null +++ b/src/providers/postgres/qgscolumntypethread.cpp @@ -0,0 +1,64 @@ +/*************************************************************************** + qgscolumntypethread.cpp - lookup postgres geometry type and srid in a thread + ------------------- +begin : 3.1.2012 +copyright : (C) 2012 by Juergen E. Fischer +email : jef at norbit dot de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgscolumntypethread.h" + +#include + +QgsGeomColumnTypeThread::QgsGeomColumnTypeThread( QgsPostgresConn *conn, bool useEstimatedMetaData ) + : QThread() + , mConn( conn ) + , mUseEstimatedMetadata( useEstimatedMetaData ) +{ + qRegisterMetaType( "QgsPostgresLayerProperty" ); +} + +void QgsGeomColumnTypeThread::addGeometryColumn( QgsPostgresLayerProperty layerProperty ) +{ + layerProperties << layerProperty; +} + +void QgsGeomColumnTypeThread::stop() +{ + mStopped = true; +} + +void QgsGeomColumnTypeThread::run() +{ + if ( !mConn ) + return; + + mStopped = false; + + foreach( QgsPostgresLayerProperty layerProperty, layerProperties ) + { + if ( !mStopped ) + { + mConn->retrieveLayerTypes( layerProperty, mUseEstimatedMetadata ); + } + else + { + layerProperty.type = ""; + } + + // Now tell the layer list dialog box... + emit setLayerType( layerProperty ); + } + + mConn->disconnect(); + mConn = 0; +} diff --git a/src/providers/postgres/qgscolumntypethread.h b/src/providers/postgres/qgscolumntypethread.h new file mode 100644 index 00000000000..045086323f4 --- /dev/null +++ b/src/providers/postgres/qgscolumntypethread.h @@ -0,0 +1,53 @@ +/*************************************************************************** + qgscolumntypethread.cpp - lookup postgres geometry type and srid in a thread + ------------------- + begin : 3.1.2012 + copyright : (C) 2012 by Juergen E. Fischer + email : jef at norbit dot de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +#ifndef QGSCOLUMNTYPETHREAD_H +#define QGSCOLUMNTYPETHREAD_H + +#include +#include "qgspostgresconn.h" + +// A class that determines the geometry type of a given database +// schema.table.column, with the option of doing so in a separate +// thread. + +class QgsGeomColumnTypeThread : public QThread +{ + Q_OBJECT + public: + QgsGeomColumnTypeThread( QgsPostgresConn *conn, bool useEstimatedMetaData ); + void addGeometryColumn( QgsPostgresLayerProperty layerProperty ); + + // These functions get the layer types and pass that information out + // by emitting the setLayerType() signal. + virtual void run(); + + signals: + void setLayerType( QgsPostgresLayerProperty layerProperty ); + + public slots: + void stop(); + + private: + QgsGeomColumnTypeThread() {} + + QgsPostgresConn *mConn; + bool mUseEstimatedMetadata; + bool mStopped; + QList layerProperties; +}; + +#endif // QGSCOLUMNTYPETHREAD_H diff --git a/src/providers/postgres/qgsdbfilterproxymodel.cpp b/src/providers/postgres/qgsdbfilterproxymodel.cpp new file mode 100644 index 00000000000..b1cdb81dab5 --- /dev/null +++ b/src/providers/postgres/qgsdbfilterproxymodel.cpp @@ -0,0 +1,53 @@ +/*************************************************************************** + qgsdbfilterproxymodel.cpp - description + ------------------------- + begin : Dec 2007 + copyright : (C) 2007 by Marco Hugentobler + email : marco dot hugentobler at karto dot baug dot ethz dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgsdbfilterproxymodel.h" + +QgsDbFilterProxyModel::QgsDbFilterProxyModel( QObject* parent ): QSortFilterProxyModel( parent ) +{ + +} + +QgsDbFilterProxyModel::~QgsDbFilterProxyModel() +{ + +} + +bool QgsDbFilterProxyModel::filterAcceptsRow( int row, const QModelIndex & source_parent ) const +{ + //if parent is valid, we have a toplevel item that should be always shown + if ( !source_parent.isValid() ) + { + return true; + } + + //else we have a row that describes a table and that + //should be tested using the given wildcard/regexp + return QSortFilterProxyModel::filterAcceptsRow( row, source_parent ); +} + +void QgsDbFilterProxyModel::_setFilterWildcard( const QString& pattern ) +{ + QSortFilterProxyModel::setFilterWildcard( pattern ); + emit layoutChanged(); +} + +void QgsDbFilterProxyModel::_setFilterRegExp( const QString& pattern ) +{ + QSortFilterProxyModel::setFilterRegExp( pattern ); + emit layoutChanged(); +} diff --git a/src/providers/postgres/qgsdbfilterproxymodel.h b/src/providers/postgres/qgsdbfilterproxymodel.h new file mode 100644 index 00000000000..01960150b30 --- /dev/null +++ b/src/providers/postgres/qgsdbfilterproxymodel.h @@ -0,0 +1,39 @@ +/*************************************************************************** + qgsdbfilterproxymodel.h - description + ----------------------- + begin : Dec 2007 + copyright : (C) 2007 by Marco Hugentobler + email : marco dot hugentobler at karto dot baug dot ethz dot ch + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef QGSDBFILTERPROXYMODEL_H +#define QGSDBFILTERPROXYMODEL_H + +#include + +/**A class that implements a custom filter and can be used + as a proxy for QgsDbTableModel*/ +class CORE_EXPORT QgsDbFilterProxyModel: public QSortFilterProxyModel +{ + public: + QgsDbFilterProxyModel( QObject* parent = 0 ); + ~QgsDbFilterProxyModel(); + /**Calls QSortFilterProxyModel::setFilterWildcard and triggers update*/ + void _setFilterWildcard( const QString& pattern ); + /**Calls QSortFilterProxyModel::setFilterRegExp and triggers update*/ + void _setFilterRegExp( const QString& pattern ); + + protected: + virtual bool filterAcceptsRow( int row, const QModelIndex & source_parent ) const; +}; + +#endif diff --git a/src/providers/postgres/qgspgnewconnection.cpp b/src/providers/postgres/qgspgnewconnection.cpp index 96f82619d51..f6ec0e1d1ca 100644 --- a/src/providers/postgres/qgspgnewconnection.cpp +++ b/src/providers/postgres/qgspgnewconnection.cpp @@ -22,13 +22,7 @@ #include "qgspgnewconnection.h" #include "qgscontexthelp.h" #include "qgsdatasourceuri.h" -#include "qgslogger.h" -#include "qgscredentialdialog.h" - -extern "C" -{ -#include -} +#include "qgspostgresconn.h" QgsPgNewConnection::QgsPgNewConnection( QWidget *parent, const QString& connName, Qt::WFlags fl ) : QDialog( parent, fl ), mOriginalConnName( connName ) @@ -175,63 +169,23 @@ void QgsPgNewConnection::testConnection() ( QgsDataSourceURI::SSLmode ) cbxSSLmode->itemData( cbxSSLmode->currentIndex() ).toInt() ); } QString conninfo = uri.connectionInfo(); - QgsDebugMsg( "PQconnectdb(\"" + conninfo + "\");" ); - PGconn *pd = PQconnectdb( conninfo.toLocal8Bit() ); // use what is set based on locale; after connecting, use Utf8 - // check the connection status - if ( PQstatus( pd ) != CONNECTION_OK ) - { - QString username = txtUsername->text(); - QString password = txtPassword->text(); + QgsPostgresConn *conn = QgsPostgresConn::connectDb( conninfo, true ); - uri.setUsername( "" ); - uri.setPassword( "" ); - - while ( PQstatus( pd ) != CONNECTION_OK ) - { - bool ok = QgsCredentials::instance()->get( conninfo, username, password, QString::fromUtf8( PQerrorMessage( pd ) ) ); - if ( !ok ) - break; - - ::PQfinish( pd ); - - QgsDataSourceURI uri( conninfo ); - - if ( !username.isEmpty() ) - uri.setUsername( username ); - - if ( !password.isEmpty() ) - uri.setPassword( password ); - - QgsDebugMsg( "PQconnectdb(\"" + uri.connectionInfo() + "\");" ); - pd = PQconnectdb( uri.connectionInfo().toLocal8Bit() ); - } - - if ( PQstatus( pd ) == CONNECTION_OK ) - { - if ( chkStoreUsername->isChecked() ) - txtUsername->setText( username ); - if ( chkStorePassword->isChecked() ) - txtPassword->setText( password ); - - QgsCredentials::instance()->put( conninfo, username, password ); - } - } - - if ( PQstatus( pd ) == CONNECTION_OK ) + if ( conn ) { // Database successfully opened; we can now issue SQL commands. QMessageBox::information( this, tr( "Test connection" ), tr( "Connection to %1 was successful" ).arg( txtDatabase->text() ) ); + + // free pg connection resources + conn->disconnect(); } else { QMessageBox::information( this, tr( "Test connection" ), - tr( "Connection failed - Check settings and try again.\n\nExtended error information:\n%1" ) - .arg( QString::fromUtf8( PQerrorMessage( pd ) ) ) ); + tr( "Connection failed - Check settings and try again.\n\n" ) ); } - // free pg connection resources - PQfinish( pd ); } diff --git a/src/providers/postgres/qgspgsourceselect.cpp b/src/providers/postgres/qgspgsourceselect.cpp index c3f5c931bf6..4550c6be8eb 100644 --- a/src/providers/postgres/qgspgsourceselect.cpp +++ b/src/providers/postgres/qgspgsourceselect.cpp @@ -21,14 +21,13 @@ email : sherman at mrcc.com #include "qgslogger.h" #include "qgsapplication.h" #include "qgscontexthelp.h" -#include "qgspostgresconnection.h" #include "qgspostgresprovider.h" #include "qgspgnewconnection.h" #include "qgsmanageconnectionsdialog.h" #include "qgsquerybuilder.h" #include "qgsdatasourceuri.h" #include "qgsvectorlayer.h" -#include "qgscredentials.h" +#include "qgscolumntypethread.h" #include #include @@ -38,13 +37,76 @@ email : sherman at mrcc.com #include #include -#ifdef HAVE_PGCONFIG -#include -#endif +/** Used to create an editor for when the user tries to change the contents of a cell */ +QWidget *QgsPgSourceSelectDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + Q_UNUSED( option ); + if ( index.column() == QgsPgTableModel::dbtmSql ) + { + QLineEdit *le = new QLineEdit( parent ); + le->setText( index.data( Qt::DisplayRole ).toString() ); + return le; + } -// Note: Because the the geometry type select SQL is also in the qgspostgresprovider -// code this parameter is duplicated there. -static const int sGeomTypeSelectLimit = 100; + if ( index.column() == QgsPgTableModel::dbtmType && index.data( Qt::UserRole + 1 ).toBool() ) + { + QComboBox *cb = new QComboBox( parent ); + foreach( QGis::WkbType type, + QList() + << QGis::WKBPoint + << QGis::WKBLineString + << QGis::WKBPolygon + << QGis::WKBMultiPoint + << QGis::WKBMultiLineString + << QGis::WKBMultiPolygon + << QGis::WKBNoGeometry ) + { + cb->addItem( QgsPgTableModel::iconForType( type ), QgsPgTableModel::displayStringForType( type ).toUpper(), type ); + } + cb->setCurrentIndex( cb->findData( index.data( Qt::UserRole + 2 ).toInt() ) ); + return cb; + } + + if ( index.column() == QgsPgTableModel::dbtmPkCol ) + { + QStringList values = index.data( Qt::UserRole + 1 ).toStringList(); + + if ( values.size() > 0 ) + { + QComboBox *cb = new QComboBox( parent ); + cb->addItems( values ); + cb->setCurrentIndex( cb->findText( index.data( Qt::DisplayRole ).toString() ) ); + return cb; + } + } + + return 0; +} + +void QgsPgSourceSelectDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ + QComboBox *cb = qobject_cast( editor ); + if ( cb ) + { + if ( index.column() == QgsPgTableModel::dbtmType ) + { + QGis::WkbType type = ( QGis::WkbType ) cb->itemData( cb->currentIndex() ).toInt(); + + model->setData( index, QgsPgTableModel::iconForType( type ), Qt::DecorationRole ); + model->setData( index, type != QGis::WKBUnknown ? QgsPgTableModel::displayStringForType( type ) : tr( "Select..." ) ); + model->setData( index, type, Qt::UserRole + 2 ); + } + else if ( index.column() == QgsPgTableModel::dbtmPkCol ) + { + model->setData( index, cb->currentText() ); + model->setData( index, cb->currentText(), Qt::UserRole + 2 ); + } + } + + QLineEdit *le = qobject_cast( editor ); + if ( le ) + model->setData( index, le->text() ); +} QgsPgSourceSelect::QgsPgSourceSelect( QWidget *parent, Qt::WFlags fl, bool managerMode, bool embeddedMode ) : QDialog( parent, fl ) @@ -86,6 +148,7 @@ QgsPgSourceSelect::QgsPgSourceSelect( QWidget *parent, Qt::WFlags fl, bool manag mSearchColumnComboBox->addItem( tr( "Type" ) ); mSearchColumnComboBox->addItem( tr( "Geometry column" ) ); mSearchColumnComboBox->addItem( tr( "Primary key column" ) ); + mSearchColumnComboBox->addItem( tr( "SRID" ) ); mSearchColumnComboBox->addItem( tr( "Sql" ) ); mProxyModel.setParent( this ); @@ -214,7 +277,7 @@ void QgsPgSourceSelect::on_btnEdit_clicked() void QgsPgSourceSelect::on_cmbConnections_activated( int ) { // Remember which database was selected. - QgsPostgresConnection::setSelectedConnection( cmbConnections->currentText() ); + QgsPostgresConn::setSelectedConnection( cmbConnections->currentText() ); cbxAllowGeometrylessTables->blockSignals( true ); QSettings settings; @@ -270,27 +333,31 @@ void QgsPgSourceSelect::on_mSearchColumnComboBox_currentIndexChanged( const QStr } else if ( text == tr( "Schema" ) ) { - mProxyModel.setFilterKeyColumn( QgsDbTableModel::dbtmSchema ); + mProxyModel.setFilterKeyColumn( QgsPgTableModel::dbtmSchema ); } else if ( text == tr( "Table" ) ) { - mProxyModel.setFilterKeyColumn( QgsDbTableModel::dbtmTable ); + mProxyModel.setFilterKeyColumn( QgsPgTableModel::dbtmTable ); } else if ( text == tr( "Type" ) ) { - mProxyModel.setFilterKeyColumn( QgsDbTableModel::dbtmType ); + mProxyModel.setFilterKeyColumn( QgsPgTableModel::dbtmType ); } else if ( text == tr( "Geometry column" ) ) { - mProxyModel.setFilterKeyColumn( QgsDbTableModel::dbtmGeomCol ); + mProxyModel.setFilterKeyColumn( QgsPgTableModel::dbtmGeomCol ); } else if ( text == tr( "Primary key column" ) ) { - mProxyModel.setFilterKeyColumn( QgsDbTableModel::dbtmPkCol ); + mProxyModel.setFilterKeyColumn( QgsPgTableModel::dbtmPkCol ); + } + else if ( text == tr( "SRID" ) ) + { + mProxyModel.setFilterKeyColumn( QgsPgTableModel::dbtmSrid ); } else if ( text == tr( "Sql" ) ) { - mProxyModel.setFilterKeyColumn( QgsDbTableModel::dbtmSql ); + mProxyModel.setFilterKeyColumn( QgsPgTableModel::dbtmSql ); } } @@ -300,13 +367,11 @@ void QgsPgSourceSelect::on_mSearchModeComboBox_currentIndexChanged( const QStrin on_mSearchTableEdit_textChanged( mSearchTableEdit->text() ); } -void QgsPgSourceSelect::setLayerType( QString schema, - QString table, QString column, - QString type ) +void QgsPgSourceSelect::setLayerType( QgsPostgresLayerProperty layerProperty ) { - mTableModel.setGeometryTypesForTable( schema, table, column, type ); - mTablesTreeView->sortByColumn( QgsDbTableModel::dbtmTable, Qt::AscendingOrder ); - mTablesTreeView->sortByColumn( QgsDbTableModel::dbtmSchema, Qt::AscendingOrder ); + mTableModel.setGeometryTypesForTable( layerProperty ); + mTablesTreeView->sortByColumn( QgsPgTableModel::dbtmTable, Qt::AscendingOrder ); + mTablesTreeView->sortByColumn( QgsPgTableModel::dbtmSchema, Qt::AscendingOrder ); } QgsPgSourceSelect::~QgsPgSourceSelect() @@ -315,8 +380,6 @@ QgsPgSourceSelect::~QgsPgSourceSelect() { mColumnTypeThread->stop(); mColumnTypeThread->wait(); - delete mColumnTypeThread; - mColumnTypeThread = NULL; } QSettings settings; @@ -330,7 +393,7 @@ QgsPgSourceSelect::~QgsPgSourceSelect() void QgsPgSourceSelect::populateConnectionList() { - QStringList keys = QgsPostgresConnection::connectionList(); + QStringList keys = QgsPostgresConn::connectionList(); QStringList::Iterator it = keys.begin(); cmbConnections->clear(); while ( it != keys.end() ) @@ -349,47 +412,31 @@ void QgsPgSourceSelect::populateConnectionList() QString QgsPgSourceSelect::layerURI( const QModelIndex &index ) { - QString schemaName = mTableModel.itemFromIndex( index.sibling( index.row(), QgsDbTableModel::dbtmSchema ) )->text(); - QString tableName = mTableModel.itemFromIndex( index.sibling( index.row(), QgsDbTableModel::dbtmTable ) )->text(); - QString geomColumnName = mTableModel.itemFromIndex( index.sibling( index.row(), QgsDbTableModel::dbtmGeomCol ) )->text(); - QString pkColumnName = mTableModel.itemFromIndex( index.sibling( index.row(), QgsDbTableModel::dbtmPkCol ) )->text(); - bool selectAtId = mTableModel.itemFromIndex( index.sibling( index.row(), QgsDbTableModel::dbtmSelectAtId ) )->checkState() == Qt::Checked; - QString sql = mTableModel.itemFromIndex( index.sibling( index.row(), QgsDbTableModel::dbtmSql ) )->text(); + QGis::WkbType geomType = ( QGis::WkbType ) mTableModel.itemFromIndex( index.sibling( index.row(), QgsPgTableModel::dbtmType ) )->data( Qt::UserRole + 2 ).toInt(); + if ( geomType == QGis::WKBUnknown ) + // no geometry type selected + return QString::null; - if ( geomColumnName.contains( " AS " ) ) - { - int a = geomColumnName.indexOf( " AS " ); - QString typeName = geomColumnName.mid( a + 4 ); //only the type name - geomColumnName = geomColumnName.left( a ); //only the geom column name - QString geomFilter; + QStandardItem *pkItem = mTableModel.itemFromIndex( index.sibling( index.row(), QgsPgTableModel::dbtmPkCol ) ); + QString pkColumnName = pkItem->data( Qt::UserRole + 2 ).toString(); - if ( typeName == "POINT" ) - { - geomFilter = QString( "upper(geometrytype(\"%1\")) IN ('POINT','MULTIPOINT')" ).arg( geomColumnName ); - } - else if ( typeName == "LINESTRING" ) - { - geomFilter = QString( "upper(geometrytype(\"%1\")) IN ('LINESTRING','MULTILINESTRING')" ).arg( geomColumnName ); - } - else if ( typeName == "POLYGON" ) - { - geomFilter = QString( "upper(geometrytype(\"%1\")) IN ('POLYGON','MULTIPOLYGON')" ).arg( geomColumnName ); - } + if ( pkItem->data( Qt::UserRole + 1 ).toStringList().size() > 0 && pkColumnName.isEmpty() ) + // no primary key for view selected + return QString::null; - if ( !geomFilter.isEmpty() && !sql.contains( geomFilter ) ) - { - if ( !sql.isEmpty() ) - { - sql += " AND "; - } + QString schemaName = mTableModel.itemFromIndex( index.sibling( index.row(), QgsPgTableModel::dbtmSchema ) )->text(); + QString tableName = mTableModel.itemFromIndex( index.sibling( index.row(), QgsPgTableModel::dbtmTable ) )->text(); + QString geomColumnName = mTableModel.itemFromIndex( index.sibling( index.row(), QgsPgTableModel::dbtmGeomCol ) )->text(); - sql += geomFilter; - } - } + QString srid = mTableModel.itemFromIndex( index.sibling( index.row(), QgsPgTableModel::dbtmSrid ) )->text(); + bool selectAtId = mTableModel.itemFromIndex( index.sibling( index.row(), QgsPgTableModel::dbtmSelectAtId ) )->checkState() == Qt::Checked; + QString sql = mTableModel.itemFromIndex( index.sibling( index.row(), QgsPgTableModel::dbtmSql ) )->text(); QgsDataSourceURI uri( m_connInfo ); - uri.setDataSource( schemaName, tableName, geomColumnName, sql, pkColumnName ); + uri.setDataSource( schemaName, tableName, geomType != QGis::WKBNoGeometry ? geomColumnName : QString::null, sql, pkColumnName ); uri.setUseEstimatedMetadata( mUseEstimatedMetadata ); + uri.setGeometryType( geomType ); + uri.setSrid( srid ); uri.disableSelectAtId( !selectAtId ); return uri.uri(); @@ -412,7 +459,11 @@ void QgsPgSourceSelect::addTables() } QModelIndex index = mProxyModel.mapToSource( *selected_it ); - m_selectedTables << layerURI( index ); + QString uri = layerURI( index ); + if ( uri.isNull() ) + continue; + + m_selectedTables << uri; } if ( m_selectedTables.empty() ) @@ -433,23 +484,22 @@ void QgsPgSourceSelect::on_btnConnect_clicked() if ( mColumnTypeThread ) { mColumnTypeThread->stop(); - mColumnTypeThread = 0; + return; } QModelIndex rootItemIndex = mTableModel.indexFromItem( mTableModel.invisibleRootItem() ); mTableModel.removeRows( 0, mTableModel.rowCount( rootItemIndex ), rootItemIndex ); // populate the table list - QgsPostgresConnection connection( cmbConnections->currentText() ); - QgsDataSourceURI uri( connection.connectionInfo() ); + QgsDataSourceURI uri = QgsPostgresConn::connUri( cmbConnections->currentText() ); QgsDebugMsg( "Connection info: " + uri.connectionInfo() ); m_connInfo = uri.connectionInfo(); mUseEstimatedMetadata = uri.useEstimatedMetadata(); - QgsPostgresProvider *pgProvider = connection.provider(); - if ( pgProvider ) + QgsPostgresConn *conn = QgsPostgresConn::connectDb( uri.connectionInfo(), true ); + if ( conn ) { QApplication::setOverrideCursor( Qt::WaitCursor ); @@ -461,43 +511,47 @@ void QgsPgSourceSelect::on_btnConnect_clicked() bool allowGeometrylessTables = cbxAllowGeometrylessTables->isChecked(); QVector layers; - if ( pgProvider->supportedLayers( layers, searchGeometryColumnsOnly, searchPublicOnly, allowGeometrylessTables ) ) + if ( conn->supportedLayers( layers, searchGeometryColumnsOnly, searchPublicOnly, allowGeometrylessTables ) ) { // Add the supported layers to the table foreach( QgsPostgresLayerProperty layer, layers ) { QString type = layer.type; - if ( !searchGeometryColumnsOnly && layer.geometryColName != QString::null ) + if ( !searchGeometryColumnsOnly && !layer.geometryColName.isNull() ) { if ( type == "GEOMETRY" || type == QString::null ) { - addSearchGeometryColumn( layer.schemaName, layer.tableName, layer.geometryColName ); - type = tr( "Waiting" ); + addSearchGeometryColumn( layer ); + type = ""; } } QgsDebugMsg( QString( "adding table %1.%2" ).arg( layer.schemaName ).arg( layer.tableName ) ); - mTableModel.addTableEntry( type, layer.schemaName, layer.tableName, layer.geometryColName, layer.pkCols, layer.sql ); + + layer.type = type; + mTableModel.addTableEntry( layer ); } // Start the thread that gets the geometry type for relations that // may take a long time to return - if ( mColumnTypeThread != NULL ) + if ( mColumnTypeThread ) { - connect( mColumnTypeThread, SIGNAL( setLayerType( QString, QString, QString, QString ) ), - this, SLOT( setLayerType( QString, QString, QString, QString ) ) ); + connect( mColumnTypeThread, SIGNAL( setLayerType( QgsPostgresLayerProperty ) ), + this, SLOT( setLayerType( QgsPostgresLayerProperty ) ) ); + connect( mColumnTypeThread, SIGNAL( finished() ), + this, SLOT( columnThreadFinished() ) ); + + btnConnect->setText( tr( "Stop" ) ); // Do it in a thread. mColumnTypeThread->start(); } } - // BEGIN CHANGES ECOS if ( cmbConnections->count() > 0 ) mAddButton->setEnabled( true ); - // END CHANGES ECOS - mTablesTreeView->sortByColumn( QgsDbTableModel::dbtmTable, Qt::AscendingOrder ); - mTablesTreeView->sortByColumn( QgsDbTableModel::dbtmSchema, Qt::AscendingOrder ); + mTablesTreeView->sortByColumn( QgsPgTableModel::dbtmTable, Qt::AscendingOrder ); + mTablesTreeView->sortByColumn( QgsPgTableModel::dbtmSchema, Qt::AscendingOrder ); //if we have only one schema item, expand it by default int numTopLevelItems = mTableModel.invisibleRootItem()->rowCount(); @@ -510,8 +564,10 @@ void QgsPgSourceSelect::on_btnConnect_clicked() } } - delete pgProvider; - QApplication::restoreOverrideCursor(); + conn->disconnect(); + + if ( !mColumnTypeThread ) + QApplication::restoreOverrideCursor(); } else { @@ -522,6 +578,14 @@ void QgsPgSourceSelect::on_btnConnect_clicked() } } +void QgsPgSourceSelect::columnThreadFinished() +{ + delete mColumnTypeThread; + mColumnTypeThread = 0; + btnConnect->setText( tr( "Connect" ) ); + QApplication::restoreOverrideCursor(); +} + QStringList QgsPgSourceSelect::selectedTables() { return m_selectedTables; @@ -541,7 +605,7 @@ void QgsPgSourceSelect::setSql( const QModelIndex &index ) } QModelIndex idx = mProxyModel.mapToSource( index ); - QString tableName = mTableModel.itemFromIndex( idx.sibling( idx.row(), QgsDbTableModel::dbtmTable ) )->text(); + QString tableName = mTableModel.itemFromIndex( idx.sibling( idx.row(), QgsPgTableModel::dbtmTable ) )->text(); QgsVectorLayer *vlayer = new QgsVectorLayer( layerURI( idx ), tableName, "postgres" ); @@ -562,31 +626,34 @@ void QgsPgSourceSelect::setSql( const QModelIndex &index ) delete vlayer; } -void QgsPgSourceSelect::addSearchGeometryColumn( const QString &schema, const QString &table, const QString &column ) +void QgsPgSourceSelect::addSearchGeometryColumn( QgsPostgresLayerProperty layerProperty ) { // store the column details and do the query in a thread - if ( mColumnTypeThread == NULL ) + if ( !mColumnTypeThread ) { - mColumnTypeThread = new QgsGeomColumnTypeThread(); - mColumnTypeThread->setConnInfo( m_connInfo, mUseEstimatedMetadata ); + QgsPostgresConn *conn = QgsPostgresConn::connectDb( m_connInfo, true /* readonly */ ); + if ( conn ) + { + mColumnTypeThread = new QgsGeomColumnTypeThread( conn, mUseEstimatedMetadata ); + } } - mColumnTypeThread->addGeometryColumn( schema, table, column ); + mColumnTypeThread->addGeometryColumn( layerProperty ); } QString QgsPgSourceSelect::fullDescription( QString schema, QString table, QString column, QString type ) { QString full_desc = ""; - if ( schema.length() > 0 ) - full_desc = '"' + schema + "\".\""; - full_desc += table + "\" (" + column + ") " + type; + if ( !schema.isEmpty() ) + full_desc = QgsPostgresConn::quotedIdentifier( schema ) + "."; + full_desc += QgsPostgresConn::quotedIdentifier( table ) + " (" + column + ") " + type; return full_desc; } void QgsPgSourceSelect::setConnectionListPosition() { // If possible, set the item currently displayed database - QString toSelect = QgsPostgresConnection::selectedConnection(); + QString toSelect = QgsPostgresConn::selectedConnection(); cmbConnections->setCurrentIndex( cmbConnections->findText( toSelect ) ); if ( cmbConnections->currentIndex() < 0 ) @@ -602,112 +669,3 @@ void QgsPgSourceSelect::setSearchExpression( const QString& regexp ) { Q_UNUSED( regexp ); } - -void QgsGeomColumnTypeThread::setConnInfo( QString conninfo, bool useEstimatedMetadata ) -{ - mConnInfo = conninfo; - mUseEstimatedMetadata = useEstimatedMetadata; -} - -void QgsGeomColumnTypeThread::addGeometryColumn( QString schema, QString table, QString column ) -{ - schemas.push_back( schema ); - tables.push_back( table ); - columns.push_back( column ); -} - -void QgsGeomColumnTypeThread::stop() -{ - mStopped = true; -} - -void QgsGeomColumnTypeThread::getLayerTypes() -{ - mStopped = false; - - PGconn *pd = PQconnectdb( mConnInfo.toLocal8Bit() ); - // check the connection status - if ( PQstatus( pd ) != CONNECTION_OK ) - { - PQfinish( pd ); - - QgsDataSourceURI uri( mConnInfo ); - QString username = uri.username(); - QString password = uri.password(); - - // use cached credentials - bool ok = QgsCredentials::instance()->get( mConnInfo, username, password, QString::fromUtf8( PQerrorMessage( pd ) ) ); - if ( !ok ) - return; - - if ( !username.isEmpty() ) - uri.setUsername( username ); - - if ( !password.isEmpty() ) - uri.setPassword( password ); - - pd = PQconnectdb( uri.connectionInfo().toLocal8Bit() ); - if ( PQstatus( pd ) == CONNECTION_OK ) - QgsCredentials::instance()->put( mConnInfo, username, password ); - - } - - if ( PQstatus( pd ) == CONNECTION_OK ) - { - PQsetClientEncoding( pd, QString( "UNICODE" ).toLocal8Bit() ); - - PGresult *res = PQexec( pd, "SET application_name='Quantum GIS'" ); - if ( !res || PQresultStatus( res ) != PGRES_COMMAND_OK ) - { - PQclear( res ); - res = PQexec( pd, "ROLLBACK" ); - } - PQclear( res ); - - for ( uint i = 0; i < schemas.size() && !mStopped; i++ ) - { - QString query = QString( "select distinct " - "case" - " when upper(geometrytype(%1)) IN ('POINT','MULTIPOINT') THEN 'POINT'" - " when upper(geometrytype(%1)) IN ('LINESTRING','MULTILINESTRING') THEN 'LINESTRING'" - " when upper(geometrytype(%1)) IN ('POLYGON','MULTIPOLYGON') THEN 'POLYGON'" - " end " - "from " ).arg( "\"" + columns[i] + "\"" ); - if ( mUseEstimatedMetadata ) - { - query += QString( "(select %1 from %2 where %1 is not null limit %3) as t" ) - .arg( "\"" + columns[i] + "\"" ) - .arg( "\"" + schemas[i] + "\".\"" + tables[i] + "\"" ) - .arg( sGeomTypeSelectLimit ); - } - else - { - query += "\"" + schemas[i] + "\".\"" + tables[i] + "\""; - } - - QgsDebugMsg( "sql: " + query ); - - PGresult *gresult = PQexec( pd, query.toUtf8() ); - QString type; - if ( PQresultStatus( gresult ) == PGRES_TUPLES_OK ) - { - QStringList types; - - for ( int j = 0; j < PQntuples( gresult ); j++ ) - { - QString type = QString::fromUtf8( PQgetvalue( gresult, j, 0 ) ); - if ( type != "" ) - types += type; - } - - type = types.join( "," ); - } - PQclear( gresult ); - - // Now tell the layer list dialog box... - emit setLayerType( schemas[i], tables[i], columns[i], type ); - } - } - - PQfinish( pd ); -} diff --git a/src/providers/postgres/qgspgsourceselect.h b/src/providers/postgres/qgspgsourceselect.h index ca9277b6eec..6b60c1723d7 100644 --- a/src/providers/postgres/qgspgsourceselect.h +++ b/src/providers/postgres/qgspgsourceselect.h @@ -16,18 +16,13 @@ ***************************************************************************/ #ifndef QGSPGSOURCESELECT_H #define QGSPGSOURCESELECT_H + #include "ui_qgsdbsourceselectbase.h" #include "qgisgui.h" #include "qgsdbfilterproxymodel.h" -#include "qgsdbtablemodel.h" +#include "qgspgtablemodel.h" #include "qgscontexthelp.h" -extern "C" -{ -#include -} - -#include #include #include #include @@ -37,58 +32,19 @@ class QPushButton; class QStringList; class QgsGeomColumnTypeThread; class QgisApp; +class QgsPgSourceSelect; class QgsPgSourceSelectDelegate : public QItemDelegate { Q_OBJECT; public: - QgsPgSourceSelectDelegate( QObject *parent = NULL ) : QItemDelegate( parent ) - { - } + QgsPgSourceSelectDelegate( QObject *parent = NULL ) + : QItemDelegate( parent ) + {} - /** Used to create an editor for when the user tries to - * change the contents of a cell */ - QWidget *createEditor( - QWidget *parent, - const QStyleOptionViewItem &option, - const QModelIndex &index ) const - { - Q_UNUSED( option ); - if ( index.column() == QgsDbTableModel::dbtmSql ) - { - QLineEdit *le = new QLineEdit( parent ); - le->setText( index.data( Qt::DisplayRole ).toString() ); - return le; - } - - - if ( index.column() == QgsDbTableModel::dbtmPkCol ) - { - QStringList values = index.data( Qt::UserRole + 1 ).toStringList(); - - if ( values.size() > 0 ) - { - QComboBox *cb = new QComboBox( parent ); - cb->addItems( values ); - cb->setCurrentIndex( cb->findText( index.data( Qt::DisplayRole ).toString() ) ); - return cb; - } - } - - return NULL; - } - - void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const - { - QComboBox *cb = qobject_cast( editor ); - if ( cb ) - model->setData( index, cb->currentText() ); - - QLineEdit *le = qobject_cast( editor ); - if ( le ) - model->setData( index, le->text() ); - } + QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const; + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const; }; @@ -108,7 +64,6 @@ class QgsPgSourceSelect : public QDialog, private Ui::QgsDbSourceSelectBase //! static function to delete a connection static void deleteConnection( QString key ); - //! Constructor QgsPgSourceSelect( QWidget *parent = 0, Qt::WFlags fl = QgisGui::ModalDialogFlags, bool managerMode = false, bool embeddedMode = false ); //! Destructor @@ -121,8 +76,7 @@ class QgsPgSourceSelect : public QDialog, private Ui::QgsDbSourceSelectBase QString connectionInfo(); signals: - void addDatabaseLayers( QStringList const & layerPathList, - QString const & providerKey ); + void addDatabaseLayers( QStringList const & layerPathList, QString const & providerKey ); void connectionsChanged(); public slots: @@ -151,7 +105,7 @@ class QgsPgSourceSelect : public QDialog, private Ui::QgsDbSourceSelectBase void setSql( const QModelIndex& index ); //! Store the selected database void on_cmbConnections_activated( int ); - void setLayerType( QString schema, QString table, QString column, QString type ); + void setLayerType( QgsPostgresLayerProperty layerProperty ); void on_mTablesTreeView_clicked( const QModelIndex &index ); void on_mTablesTreeView_doubleClicked( const QModelIndex &index ); //!Sets a new regular expression to the model @@ -159,6 +113,8 @@ class QgsPgSourceSelect : public QDialog, private Ui::QgsDbSourceSelectBase void on_buttonBox_helpRequested() { QgsContextHelp::run( metaObject()->className() ); } + void columnThreadFinished(); + private: typedef QPair geomPair; typedef QList geomCol; @@ -170,7 +126,7 @@ class QgsPgSourceSelect : public QDialog, private Ui::QgsDbSourceSelectBase bool mEmbeddedMode; // queue another query for the thread - void addSearchGeometryColumn( const QString &schema, const QString &table, const QString &column ); + void addSearchGeometryColumn( QgsPostgresLayerProperty layerProperty ); // Set the position of the database connection list to the last // used one. @@ -189,50 +145,13 @@ class QgsPgSourceSelect : public QDialog, private Ui::QgsDbSourceSelectBase QMap > mLayerIcons; //! Model that acts as datasource for mTableTreeWidget - QgsDbTableModel mTableModel; + QgsPgTableModel mTableModel; QgsDbFilterProxyModel mProxyModel; QString layerURI( const QModelIndex &index ); QPushButton *mBuildQueryButton; QPushButton *mAddButton; -}; - - -// Perhaps this class should be in its own file?? -// -// A class that determines the geometry type of a given database -// schema.table.column, with the option of doing so in a separate -// thread. - -class QgsGeomColumnTypeThread : public QThread -{ - Q_OBJECT - public: - - void setConnInfo( QString s, bool useEstimatedMetadata ); - void addGeometryColumn( QString schema, QString table, QString column ); - - // These functions get the layer types and pass that information out - // by emitting the setLayerType() signal. The getLayerTypes() - // function does the actual work, but use the run() function if you - // want the work to be done as a separate thread from the calling - // process. - virtual void run() { getLayerTypes(); } - void getLayerTypes(); - - signals: - void setLayerType( QString schema, QString table, QString column, - QString type ); - - public slots: - void stop(); - - - private: - QString mConnInfo; - bool mUseEstimatedMetadata; - bool mStopped; - std::vector schemas, tables, columns; + void updateSelectableState( const QModelIndex &index ); }; #endif // QGSPGSOURCESELECT_H diff --git a/src/core/qgsdbtablemodel.cpp b/src/providers/postgres/qgspgtablemodel.cpp similarity index 50% rename from src/core/qgsdbtablemodel.cpp rename to src/providers/postgres/qgspgtablemodel.cpp index 24ccf203d3c..3efd0c38c23 100644 --- a/src/core/qgsdbtablemodel.cpp +++ b/src/providers/postgres/qgspgtablemodel.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - qgsdbtablemodel.cpp - description + qgspgtablemodel.cpp - description ------------------- begin : Dec 2007 copyright : (C) 2007 by Marco Hugentobler @@ -15,90 +15,119 @@ * * ***************************************************************************/ -#include "qgsdbtablemodel.h" +#include "qgspgtablemodel.h" #include "qgsdataitem.h" +#include "qgslogger.h" -QgsDbTableModel::QgsDbTableModel(): QStandardItemModel(), mTableCount( 0 ) +QgsPgTableModel::QgsPgTableModel() + : QStandardItemModel() + , mTableCount( 0 ) { QStringList headerLabels; headerLabels << tr( "Schema" ); headerLabels << tr( "Table" ); headerLabels << tr( "Type" ); headerLabels << tr( "Geometry column" ); + headerLabels << tr( "SRID" ); headerLabels << tr( "Primary key column" ); headerLabels << tr( "Select at id" ); headerLabels << tr( "Sql" ); setHorizontalHeaderLabels( headerLabels ); } -QgsDbTableModel::~QgsDbTableModel() +QgsPgTableModel::~QgsPgTableModel() { - } -void QgsDbTableModel::addTableEntry( QString type, QString schemaName, QString tableName, QString geometryColName, const QStringList &pkCols, QString sql ) +void QgsPgTableModel::addTableEntry( QgsPostgresLayerProperty layerProperty ) { - //is there already a root item with the given scheme Name? - QStandardItem *schemaItem; - QList schemaItems = findItems( schemaName, Qt::MatchExactly, dbtmSchema ); + QgsDebugMsg( QString( "%1.%2.%3 type=%4 srid=%5 pk=%6 sql=%7" ) + .arg( layerProperty.schemaName ) + .arg( layerProperty.tableName ) + .arg( layerProperty.geometryColName ) + .arg( layerProperty.type ) + .arg( layerProperty.srid ) + .arg( layerProperty.pkCols.join( "," ) ) + .arg( layerProperty.sql ) ); - //there is already an item for this schema + // is there already a root item with the given scheme Name? + QStandardItem *schemaItem; + QList schemaItems = findItems( layerProperty.schemaName, Qt::MatchExactly, dbtmSchema ); + + // there is already an item for this schema if ( schemaItems.size() > 0 ) { schemaItem = schemaItems.at( dbtmSchema ); } - else //create a new toplevel item for this schema + else { - schemaItem = new QStandardItem( schemaName ); + // create a new toplevel item for this schema + schemaItem = new QStandardItem( layerProperty.schemaName ); schemaItem->setFlags( Qt::ItemIsEnabled ); invisibleRootItem()->setChild( invisibleRootItem()->rowCount(), schemaItem ); } - //path to icon for specified type - QString typeName; - - QGis::WkbType wkbType = qgisTypeFromDbType( type ); - QIcon iconFile = iconForType( wkbType ); + QGis::WkbType wkbType = qgisTypeFromDbType( layerProperty.type ); + if ( wkbType == QGis::WKBUnknown && layerProperty.geometryColName.isEmpty() ) + { + wkbType = QGis::WKBNoGeometry; + } QList childItemList; - QStandardItem* schemaNameItem = new QStandardItem( schemaName ); + QStandardItem *schemaNameItem = new QStandardItem( layerProperty.schemaName ); schemaNameItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable ); - QStandardItem* typeItem = new QStandardItem( QIcon( iconFile ), type ); - typeItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable ); + QStandardItem *typeItem = new QStandardItem( iconForType( wkbType ), wkbType == QGis::WKBUnknown ? tr( "Waiting..." ) : displayStringForType( wkbType ) ); + typeItem->setData( wkbType == QGis::WKBUnknown, Qt::UserRole + 1 ); + typeItem->setData( wkbType, Qt::UserRole + 2 ); + typeItem->setFlags(( wkbType != QGis::WKBUnknown ? Qt::ItemIsEnabled : Qt::NoItemFlags ) | Qt::ItemIsSelectable | Qt::ItemIsEditable ); - QStandardItem* tableItem = new QStandardItem( tableName ); + QStandardItem *tableItem = new QStandardItem( layerProperty.tableName ); tableItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable ); - QStandardItem* geomItem = new QStandardItem( geometryColName ); + QStandardItem *geomItem = new QStandardItem( layerProperty.geometryColName ); geomItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable ); - QStandardItem* pkItem = new QStandardItem( "" ); - pkItem->setData( pkCols ); + QStandardItem *sridItem = new QStandardItem( QString::number( layerProperty.srid ) ); + sridItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable ); + + QString pkText, pkCol = ""; + switch ( layerProperty.pkCols.size() ) + { + case 0: pkText = ""; break; + case 1: pkText = layerProperty.pkCols[0]; pkCol = pkText; break; + default: pkText = tr( "Select..." ); break; + } + + QStandardItem *pkItem = new QStandardItem( pkText ); + pkItem->setData( layerProperty.pkCols, Qt::UserRole + 1 ); + pkItem->setData( pkCol, Qt::UserRole + 2 ); pkItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable ); - QStandardItem* selItem = new QStandardItem( "" ); + QStandardItem *selItem = new QStandardItem( "" ); selItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable ); selItem->setCheckState( Qt::Checked ); selItem->setToolTip( tr( "Disable 'Fast Access to Features at ID' capability to force keeping the attribute table in memory (e.g. in case of expensive views)." ) ); - QStandardItem* sqlItem = new QStandardItem( sql ); + QStandardItem* sqlItem = new QStandardItem( layerProperty.sql ); sqlItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable ); - childItemList.push_back( schemaNameItem ); - childItemList.push_back( tableItem ); - childItemList.push_back( typeItem ); - childItemList.push_back( geomItem ); - childItemList.push_back( pkItem ); - childItemList.push_back( selItem ); - childItemList.push_back( sqlItem ); + childItemList << schemaNameItem; + childItemList << tableItem; + childItemList << typeItem; + childItemList << geomItem; + childItemList << sridItem; + childItemList << pkItem; + childItemList << selItem; + childItemList << sqlItem; schemaItem->appendRow( childItemList ); + ++mTableCount; } -void QgsDbTableModel::setSql( const QModelIndex &index, const QString &sql ) +void QgsPgTableModel::setSql( const QModelIndex &index, const QString &sql ) { if ( !index.isValid() || !index.parent().isValid() ) { @@ -126,26 +155,22 @@ void QgsDbTableModel::setSql( const QModelIndex &index, const QString &sql ) } QStandardItem* schemaItem = schemaItems.at( dbtmSchema ); - int numChildren = schemaItem->rowCount(); - QModelIndex currentChildIndex; - QModelIndex currentTableIndex; - QModelIndex currentGeomIndex; - - for ( int i = 0; i < numChildren; ++i ) + int n = schemaItem->rowCount(); + for ( int i = 0; i < n; i++ ) { - currentChildIndex = indexFromItem( schemaItem->child( i, dbtmSchema ) ); + QModelIndex currentChildIndex = indexFromItem( schemaItem->child( i, dbtmSchema ) ); if ( !currentChildIndex.isValid() ) { continue; } - currentTableIndex = currentChildIndex.sibling( i, dbtmTable ); + QModelIndex currentTableIndex = currentChildIndex.sibling( i, dbtmTable ); if ( !currentTableIndex.isValid() ) { continue; } - currentGeomIndex = currentChildIndex.sibling( i, dbtmGeomCol ); + QModelIndex currentGeomIndex = currentChildIndex.sibling( i, dbtmGeomCol ); if ( !currentGeomIndex.isValid() ) { continue; @@ -164,75 +189,82 @@ void QgsDbTableModel::setSql( const QModelIndex &index, const QString &sql ) } } -void QgsDbTableModel::setGeometryTypesForTable( const QString& schema, const QString& table, const QString& attribute, const QString& type ) +void QgsPgTableModel::setGeometryTypesForTable( QgsPostgresLayerProperty layerProperty ) { - bool typeIsEmpty = type.isEmpty(); //true means the table has no valid geometry entry and the item for this table should be removed - QStringList typeList = type.split( "," ); + QStringList typeList = layerProperty.type.split( ",", QString::SkipEmptyParts ); //find schema item and table item QStandardItem* schemaItem; - QList schemaItems = findItems( schema, Qt::MatchExactly, dbtmSchema ); + QList schemaItems = findItems( layerProperty.schemaName, Qt::MatchExactly, dbtmSchema ); if ( schemaItems.size() < 1 ) { return; } + schemaItem = schemaItems.at( 0 ); - int numChildren = schemaItem->rowCount(); - QModelIndex currentChildIndex; - QModelIndex currentTableIndex; - QModelIndex currentTypeIndex; - QModelIndex currentGeomColumnIndex; - QModelIndex currentPkColumnIndex; - - for ( int i = 0; i < numChildren; ++i ) + int n = schemaItem->rowCount(); + for ( int i = 0; i < n; i++ ) { - currentChildIndex = indexFromItem( schemaItem->child( i, dbtmSchema ) ); + QModelIndex currentChildIndex = indexFromItem( schemaItem->child( i, dbtmSchema ) ); if ( !currentChildIndex.isValid() ) { continue; } - currentTableIndex = currentChildIndex.sibling( i, dbtmTable ); - currentTypeIndex = currentChildIndex.sibling( i, dbtmType ); - currentGeomColumnIndex = currentChildIndex.sibling( i, dbtmGeomCol ); - currentPkColumnIndex = currentChildIndex.sibling( i, dbtmPkCol ); - QString geomColText = itemFromIndex( currentGeomColumnIndex )->text(); - QStringList pkCols = itemFromIndex( currentPkColumnIndex )->data().toStringList(); - if ( !currentTypeIndex.isValid() || !currentTableIndex.isValid() || !currentGeomColumnIndex.isValid() ) + QModelIndex currentTypeIndex = currentChildIndex.sibling( i, dbtmType ); + QModelIndex currentTableIndex = currentChildIndex.sibling( i, dbtmTable ); + QModelIndex currentGeomColumnIndex = currentChildIndex.sibling( i, dbtmGeomCol ); + QModelIndex currentPkColumnIndex = currentChildIndex.sibling( i, dbtmPkCol ); + QModelIndex currentSridIndex = currentChildIndex.sibling( i, dbtmSrid ); + + if ( !currentTypeIndex.isValid() + || !currentTableIndex.isValid() + || !currentGeomColumnIndex.isValid() + || !currentPkColumnIndex.isValid() + || !currentSridIndex.isValid() + ) { continue; } - if ( itemFromIndex( currentTableIndex )->text() == table && - ( geomColText == attribute || geomColText.startsWith( attribute + " AS " ) ) ) + QString tableText = itemFromIndex( currentTableIndex )->text(); + QString geomColText = itemFromIndex( currentGeomColumnIndex )->text(); + QStandardItem *typeItem = itemFromIndex( currentTypeIndex ); + QStandardItem *sridItem = itemFromIndex( currentSridIndex ); + + if ( tableText == layerProperty.tableName && geomColText == layerProperty.geometryColName ) { - if ( typeIsEmpty ) + sridItem->setText( QString::number( layerProperty.srid ) ); + + if ( typeList.isEmpty() ) { - removeRow( i, indexFromItem( schemaItem ) ); - return; + typeItem->setText( tr( "Select..." ) ); + } + else + { + // update existing row + QGis::WkbType wkbType = qgisTypeFromDbType( typeList.at( 0 ) ); + + typeItem->setIcon( iconForType( wkbType ) ); + typeItem->setText( displayStringForType( wkbType ) ); + typeItem->setData( false, Qt::UserRole + 1 ); + typeItem->setData( wkbType, Qt::UserRole + 2 ); + + for ( int j = 1; j < typeList.size(); j++ ) + { + layerProperty.type = typeList[j]; + addTableEntry( layerProperty ); + } } - QGis::WkbType wkbType = qgisTypeFromDbType( typeList.at( 0 ) ); - QIcon myIcon = iconForType( wkbType ); - itemFromIndex( currentTypeIndex )->setText( typeList.at( 0 ) ); //todo: add other rows - itemFromIndex( currentTypeIndex )->setIcon( myIcon ); - if ( !geomColText.contains( " AS " ) ) - { - itemFromIndex( currentGeomColumnIndex )->setText( geomColText + " AS " + typeList.at( 0 ) ); - } - - for ( int j = 1; j < typeList.size(); ++j ) - { - //todo: add correct type - addTableEntry( typeList.at( j ), schema, table, geomColText + " AS " + typeList.at( j ), pkCols, "" ); - } + typeItem->setFlags( typeItem->flags() | Qt::ItemIsEnabled ); } } } -QIcon QgsDbTableModel::iconForType( QGis::WkbType type ) const +QIcon QgsPgTableModel::iconForType( QGis::WkbType type ) { if ( type == QGis::WKBPoint || type == QGis::WKBPoint25D || type == QGis::WKBMultiPoint || type == QGis::WKBMultiPoint25D ) { @@ -246,10 +278,17 @@ QIcon QgsDbTableModel::iconForType( QGis::WkbType type ) const { return QIcon( QgsDataItem::getThemePixmap( "/mIconPolygonLayer.png" ) ); } - else return QIcon(); + else if ( type == QGis::WKBNoGeometry ) + { + return QIcon( QgsDataItem::getThemePixmap( "/mIconTableLayer.png" ) ); + } + else + { + return QIcon( QgsDataItem::getThemePixmap( "/mIconLayer.png" ) ); + } } -QString QgsDbTableModel::displayStringForType( QGis::WkbType type ) const +QString QgsPgTableModel::displayStringForType( QGis::WkbType type ) { if ( type == QGis::WKBPoint || type == QGis::WKBPoint25D ) { @@ -275,11 +314,20 @@ QString QgsDbTableModel::displayStringForType( QGis::WkbType type ) const { return tr( "Multipolygon" ); } - return "Unknown"; + else if ( type == QGis::WKBNoGeometry ) + { + return tr( "No Geometry" ); + } + else + { + return tr( "Unknown" ); + } } -QGis::WkbType QgsDbTableModel::qgisTypeFromDbType( const QString& dbType ) const +QGis::WkbType QgsPgTableModel::qgisTypeFromDbType( QString dbType ) { + dbType = dbType.toUpper(); + if ( dbType == "POINT" ) { return QGis::WKBPoint; @@ -304,5 +352,8 @@ QGis::WkbType QgsDbTableModel::qgisTypeFromDbType( const QString& dbType ) const { return QGis::WKBMultiPolygon; } - return QGis::WKBUnknown; + else + { + return QGis::WKBUnknown; + } } diff --git a/src/core/qgsdbtablemodel.h b/src/providers/postgres/qgspgtablemodel.h similarity index 75% rename from src/core/qgsdbtablemodel.h rename to src/providers/postgres/qgspgtablemodel.h index 801985c976e..cbd16687f3d 100644 --- a/src/core/qgsdbtablemodel.h +++ b/src/providers/postgres/qgspgtablemodel.h @@ -1,5 +1,5 @@ /*************************************************************************** - qgsdbtablemodel.h - description + qgspgtablemodel.h - description ------------------- begin : Dec 2007 copyright : (C) 2007 by Marco Hugentobler @@ -16,28 +16,31 @@ ***************************************************************************/ #include -class QIcon; + #include "qgis.h" +#include "qgspostgresconn.h" + +class QIcon; /**A model that holds the tables of a database in a hierarchy where the schemas are the root elements that contain the individual tables as children. The tables have the following columns: Type, Schema, Tablename, Geometry Column, Sql*/ -class CORE_EXPORT QgsDbTableModel : public QStandardItemModel +class QgsPgTableModel : public QStandardItemModel { Q_OBJECT public: - QgsDbTableModel(); - ~QgsDbTableModel(); + QgsPgTableModel(); + ~QgsPgTableModel(); /**Adds entry for one database table to the model*/ - void addTableEntry( QString type, QString schemaName, QString tableName, QString geometryColName, const QStringList &pkCols, QString Sql ); + void addTableEntry( QgsPostgresLayerProperty property ); /**Sets an sql statement that belongs to a cell specified by a model index*/ void setSql( const QModelIndex& index, const QString& sql ); /**Sets one or more geometry types to a row. In case of several types, additional rows are inserted. This is for tables where the type is dectected later by thread*/ - void setGeometryTypesForTable( const QString& schema, const QString& table, const QString& attribute, const QString& type ); + void setGeometryTypesForTable( QgsPostgresLayerProperty layerProperty ); /**Returns the number of tables in the model*/ int tableCount() const { return mTableCount; } @@ -48,20 +51,19 @@ class CORE_EXPORT QgsDbTableModel : public QStandardItemModel dbtmTable, dbtmType, dbtmGeomCol, + dbtmSrid, dbtmPkCol, dbtmSelectAtId, dbtmSql, dbtmColumns }; + static QIcon iconForType( QGis::WkbType type ); + static QString displayStringForType( QGis::WkbType type ); + static QGis::WkbType qgisTypeFromDbType( QString dbType ); + private: /**Number of tables in the model*/ int mTableCount; - - QIcon iconForType( QGis::WkbType type ) const; - QString displayStringForType( QGis::WkbType type ) const; - - /**Returns qgis wkbtype from database typename*/ - QGis::WkbType qgisTypeFromDbType( const QString& dbType ) const; }; diff --git a/src/providers/postgres/qgspostgresconn.cpp b/src/providers/postgres/qgspostgresconn.cpp new file mode 100644 index 00000000000..095979fb52c --- /dev/null +++ b/src/providers/postgres/qgspostgresconn.cpp @@ -0,0 +1,1228 @@ +/*************************************************************************** + qgspostgresconn.cpp - connection class to PostgreSQL/PostGIS + ------------------- + begin : 2011/01/28 + copyright : (C) 2011 by Juergen E. Fischer + email : jef at norbit dot de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgspostgresconn.h" +#include "qgslogger.h" +#include "qgsdatasourceuri.h" +#include "qgsmessagelog.h" +#include "qgscredentials.h" +#include "qgsfield.h" +#include "qgspgtablemodel.h" + +#include + +// for htonl +#ifdef Q_OS_WIN +#include +#else +#include +#endif + +QgsPostgresResult::~QgsPostgresResult() +{ + if ( mRes ) + ::PQclear( mRes ); + mRes = 0; +} + +QgsPostgresResult &QgsPostgresResult::operator=( PGresult * theRes ) +{ + if ( mRes ) + ::PQclear( mRes ); + mRes = theRes; + return *this; +} + +QgsPostgresResult &QgsPostgresResult::operator=( const QgsPostgresResult & src ) +{ + if ( mRes ) + ::PQclear( mRes ); + mRes = src.result(); + return *this; +} + +ExecStatusType QgsPostgresResult::PQresultStatus() +{ + Q_ASSERT( mRes ); + return ::PQresultStatus( mRes ); +} + +QString QgsPostgresResult::PQresultErrorMessage() +{ + Q_ASSERT( mRes ); + return QString::fromUtf8( ::PQresultErrorMessage( mRes ) ); +} + +int QgsPostgresResult::PQntuples() +{ + Q_ASSERT( mRes ); + return ::PQntuples( mRes ); +} + +QString QgsPostgresResult::PQgetvalue( int row, int col ) +{ + Q_ASSERT( mRes ); + return PQgetisnull( row, col ) + ? QString::null + : QString::fromUtf8( ::PQgetvalue( mRes, row, col ) ); +} + +bool QgsPostgresResult::PQgetisnull( int row, int col ) +{ + Q_ASSERT( mRes ); + return ::PQgetisnull( mRes, row, col ); +} + +int QgsPostgresResult::PQnfields() +{ + Q_ASSERT( mRes ); + return ::PQnfields( mRes ); +} + +QString QgsPostgresResult::PQfname( int col ) +{ + Q_ASSERT( mRes ); + return QString::fromUtf8( ::PQfname( mRes, col ) ); +} + +int QgsPostgresResult::PQftable( int col ) +{ + Q_ASSERT( mRes ); + return ::PQftable( mRes, col ); +} + +int QgsPostgresResult::PQftype( int col ) +{ + Q_ASSERT( mRes ); + return ::PQftype( mRes, col ); +} + +QMap QgsPostgresConn::sConnectionsRO; +QMap QgsPostgresConn::sConnectionsRW; +const int QgsPostgresConn::sGeomTypeSelectLimit = 100; + +QgsPostgresConn *QgsPostgresConn::connectDb( QString conninfo, bool readonly ) +{ + QMap &connections = + readonly ? QgsPostgresConn::sConnectionsRO : QgsPostgresConn::sConnectionsRW; + + if ( connections.contains( conninfo ) ) + { + QgsDebugMsg( QString( "Using cached connection for %1" ).arg( conninfo ) ); + connections[conninfo]->mRef++; + return connections[conninfo]; + } + + QgsPostgresConn *conn = new QgsPostgresConn( conninfo, readonly ); + + if ( conn->mRef == 0 ) + { + delete conn; + return 0; + } + + connections.insert( conninfo, conn ); + + return conn; +} + +QgsPostgresConn::QgsPostgresConn( QString conninfo, bool readOnly ) + : mRef( 1 ) + , mOpenCursors( 0 ) + , mConnInfo( conninfo ) + , mGotPostgisVersion( false ) + , mReadOnly( readOnly ) +{ + QgsDebugMsg( QString( "New PostgreSQL connection for " ) + conninfo ); + + mConn = PQconnectdb( conninfo.toLocal8Bit() ); // use what is set based on locale; after connecting, use Utf8 + // check the connection status + if ( PQstatus() != CONNECTION_OK ) + { + QgsDataSourceURI uri( conninfo ); + QString username = uri.username(); + QString password = uri.password(); + + while ( PQstatus() != CONNECTION_OK ) + { + bool ok = QgsCredentials::instance()->get( conninfo, username, password, PQerrorMessage() ); + if ( !ok ) + break; + + PQfinish(); + + if ( !username.isEmpty() ) + uri.setUsername( username ); + + if ( !password.isEmpty() ) + uri.setPassword( password ); + + QgsDebugMsg( "Connecting to " + uri.connectionInfo() ); + mConn = PQconnectdb( uri.connectionInfo().toLocal8Bit() ); + } + + if ( PQstatus() == CONNECTION_OK ) + QgsCredentials::instance()->put( conninfo, username, password ); + } + + if ( PQstatus() != CONNECTION_OK ) + { + PQfinish(); + QgsMessageLog::logMessage( tr( "Connection to database failed" ), tr( "PostGIS" ) ); + mRef = 0; + return; + } + + //set client encoding to unicode because QString uses UTF-8 anyway + QgsDebugMsg( "setting client encoding to UNICODE" ); + int errcode = PQsetClientEncoding( mConn, QString( "UNICODE" ).toLocal8Bit() ); + if ( errcode == 0 ) + { + QgsDebugMsg( "encoding successfully set" ); + } + else if ( errcode == -1 ) + { + QgsMessageLog::logMessage( tr( "error in setting encoding" ), tr( "PostGIS" ) ); + } + else + { + QgsMessageLog::logMessage( tr( "undefined return value from encoding setting" ), tr( "PostGIS" ) ); + } + + QgsDebugMsg( "Connection to the database was successful" ); + + deduceEndian(); + + /* Check to see if we have working PostGIS support */ + if ( postgisVersion().isNull() ) + { + QgsMessageLog::logMessage( tr( "Your database has no working PostGIS support." ), tr( "PostGIS" ) ); + PQfinish(); + mRef = 0; + return; + } + + if ( mPostgresqlVersion >= 90000 ) + { + PQexecNR( "SET application_name='Quantum GIS'" ); + } + + /* Check to see if we have GEOS support and if not, warn the user about + the problems they will see :) */ + QgsDebugMsg( "Checking for GEOS support" ); + + if ( !hasGEOS() ) + { + QgsMessageLog::logMessage( tr( "Your PostGIS installation has no GEOS support. Feature selection and identification will not work properly. Please install PostGIS with GEOS support (http://geos.refractions.net)" ), tr( "PostGIS" ) ); + } + + if ( hasTopology() ) + { + QgsDebugMsg( "Topology support available!" ); + } +} + +QgsPostgresConn::~QgsPostgresConn() +{ + Q_ASSERT( mRef == 0 ); + if ( mConn ) + ::PQfinish( mConn ); + mConn = 0; +} + +void QgsPostgresConn::disconnect() +{ + if ( --mRef > 0 ) + return; + + QMap& connections = mReadOnly ? sConnectionsRO : sConnectionsRW; + + QString key = connections.key( this, QString::null ); + + Q_ASSERT( !key.isNull() ); + connections.remove( key ); + + deleteLater(); +} + +QStringList QgsPostgresConn::pkCandidates( QString schemaName, QString viewName ) +{ + QStringList cols; + + QString sql = QString( "SELECT attname FROM pg_attribute JOIN pg_type ON atttypid=pg_type.oid WHERE pg_type.typname IN ('int2','int4','int8','oid','serial','serial8') AND attrelid=regclass('%1.%2')" ) + .arg( quotedIdentifier( schemaName ) ) + .arg( quotedIdentifier( viewName ) ); + QgsDebugMsg( sql ); + QgsPostgresResult colRes = PQexec( sql ); + + if ( colRes.PQresultStatus() == PGRES_TUPLES_OK ) + { + for ( int i = 0; i < colRes.PQntuples(); i++ ) + { + QgsDebugMsg( colRes.PQgetvalue( i, 0 ) ); + cols << colRes.PQgetvalue( i, 0 ); + } + } + else + { + QgsMessageLog::logMessage( tr( "SQL:%1\nresult:%2\nerror:%3\n" ).arg( sql ).arg( colRes.PQresultStatus() ).arg( colRes.PQresultErrorMessage() ), tr( "PostGIS" ) ); + } + + return cols; +} + +bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchPublicOnly, bool allowGeometrylessTables ) +{ + int nColumns = 0; + int nGTables = 0; + + QgsPostgresResult result; + QgsPostgresLayerProperty layerProperty; + + QgsDebugMsg( "Entering." ); + mLayersSupported.clear(); + + for ( int i = 0; i < 2; i++ ) + { + QString gtableName, columnName; + + if ( i == 0 ) + { + gtableName = "geometry_columns"; + columnName = "f_geometry_column"; + } + else if ( i == 1 ) + { + gtableName = "geography_columns"; + columnName = "f_geography_column"; + } + + // The following query returns only tables that exist and the user has SELECT privilege on. + // Can't use regclass here because table must exist, else error occurs. + QString sql = QString( "SELECT " + "f_table_name," + "f_table_schema," + "%2," + "upper(type)," + "coalesce(srid,-1)," + "pg_class.relkind" + " FROM " + "%1,pg_class,pg_namespace" + " WHERE relname=f_table_name" + " AND f_table_schema=nspname" + " AND pg_namespace.oid=pg_class.relnamespace" + " AND has_schema_privilege(pg_namespace.nspname,'usage')" + " AND has_table_privilege('\"'||pg_namespace.nspname||'\".\"'||pg_class.relname||'\"','select')" // user has select privilege + " ORDER BY f_table_schema,f_table_name,%2" ).arg( gtableName ).arg( columnName ); + + QgsDebugMsg( "getting table info: " + sql ); + + result = PQexec( sql ); + if ( result.PQresultStatus() != PGRES_TUPLES_OK ) + { + PQexecNR( "COMMIT" ); + } + else + { + nGTables++; + + for ( int idx = 0; idx < result.PQntuples(); idx++ ) + { + QString tableName = result.PQgetvalue( idx, 0 ); + QString schemaName = result.PQgetvalue( idx, 1 ); + QString column = result.PQgetvalue( idx, 2 ); + QString type = result.PQgetvalue( idx, 3 ); + QString srid = result.PQgetvalue( idx, 4 ); + QString relkind = result.PQgetvalue( idx, 5 ); + + QgsDebugMsg( QString( "%1 : %2.%3.%4: %5 %6 %7" ) + .arg( gtableName ) + .arg( schemaName ).arg( tableName ).arg( column ) + .arg( type ) + .arg( srid ) + .arg( relkind ) ); + + layerProperty.type = type; + layerProperty.schemaName = schemaName; + layerProperty.tableName = tableName; + layerProperty.geometryColName = column; + layerProperty.pkCols = relkind == "v" ? pkCandidates( schemaName, tableName ) : QStringList(); + layerProperty.srid = srid.toInt(); + layerProperty.sql = ""; + + mLayersSupported << layerProperty; + nColumns++; + } + } + } + + if ( nColumns == 0 ) + { + QgsMessageLog::logMessage( tr( "Database connection was successful, but the accessible tables could not be determined." ), tr( "PostGIS" ) ); + nColumns = -1; + } + + //search for geometry columns in tables that are not in the geometry_columns metatable + if ( !searchGeometryColumnsOnly ) + { + // Now have a look for geometry columns that aren't in the + // geometry_columns table. This code is specific to postgresql, + // but an equivalent query should be possible in other + // databases. + QString sql = "SELECT " + "pg_class.relname" + ",pg_namespace.nspname" + ",pg_attribute.attname" + ",pg_class.relkind" + " FROM " + "pg_attribute,pg_class,pg_namespace" + " WHERE pg_namespace.oid=pg_class.relnamespace" + " AND pg_attribute.attrelid = pg_class.oid" + " AND (" + " EXISTS (SELECT * FROM pg_type WHERE pg_type.oid=pg_attribute.atttypid AND pg_type.typname IN ('geometry','geography','topogeometry'))" + " OR pg_attribute.atttypid IN (SELECT oid FROM pg_type a WHERE EXISTS (SELECT * FROM pg_type b WHERE a.typbasetype=b.oid AND b.typname IN ('geometry','geography','topogeometry')))" + ")" + " AND has_schema_privilege( pg_namespace.nspname, 'usage' )" + " AND has_table_privilege( '\"' || pg_namespace.nspname || '\".\"' || pg_class.relname || '\"', 'select' )"; + + // user has select privilege + if ( searchPublicOnly ) + sql += " AND pg_namespace.nspname='public'"; + + if ( nColumns > 0 ) + { + // TODO: handle this for the topogeometry case + sql += " AND NOT EXISTS (SELECT * FROM geometry_columns WHERE pg_namespace.nspname=f_table_schema AND pg_class.relname=f_table_name)"; + + if ( nGTables > 1 ) + { + // TODO: handle this for the topogeometry case + // TODO: handle this for the geometry case ? + sql += " AND NOT EXISTS (SELECT * FROM geography_columns WHERE pg_namespace.nspname=f_table_schema AND pg_class.relname=f_table_name)"; + } + } + else + { + nColumns = 0; + } + + sql += " AND pg_class.relkind IN ('v','r')"; // only from views and relations (tables) + + QgsDebugMsg( "sql: " + sql ); + + result = PQexec( sql ); + + if ( result.PQresultStatus() != PGRES_TUPLES_OK ) + { + QgsMessageLog::logMessage( tr( "Database connection was successful, but the accessible tables could not be determined. The error message from the database was:\n%1\n" ) + .arg( result.PQresultErrorMessage() ), + tr( "PostGIS" ) ); + if ( nColumns == 0 ) + nColumns = -1; + } + else if ( result.PQntuples() > 0 ) + { + for ( int i = 0; i < result.PQntuples(); i++ ) + { + // Have the column name, schema name and the table name. The concept of a + // catalog doesn't exist in postgresql so we ignore that, but we + // do need to get the geometry type. + + // Make the assumption that the geometry type for the first + // row is the same as for all other rows. + + QString table = result.PQgetvalue( i, 0 ); // relname + QString schema = result.PQgetvalue( i, 1 ); // nspname + QString column = result.PQgetvalue( i, 2 ); // attname + QString relkind = result.PQgetvalue( i, 3 ); // relation kind + + QgsDebugMsg( QString( "%1.%2.%3: %4" ).arg( schema ).arg( table ).arg( column ).arg( relkind ) ); + + layerProperty.type = QString::null; + layerProperty.schemaName = schema; + layerProperty.tableName = table; + layerProperty.geometryColName = column; + layerProperty.pkCols = relkind == "v" ? pkCandidates( schema, table ) : QStringList(); + layerProperty.sql = ""; + + mLayersSupported << layerProperty; + nColumns++; + } + } + } + + if ( allowGeometrylessTables ) + { + QString sql = "SELECT " + "pg_class.relname" + ",pg_namespace.nspname" + ",pg_class.relkind" + " FROM " + " pg_class" + ",pg_namespace" + " WHERE pg_namespace.oid=pg_class.relnamespace" + " AND has_schema_privilege(pg_namespace.nspname,'usage')" + " AND has_table_privilege('\"' || pg_namespace.nspname || '\".\"' || pg_class.relname || '\"','select')" + " AND pg_class.relkind IN ('v','r')"; + + // user has select privilege + if ( searchPublicOnly ) + sql += " AND pg_namespace.nspname='public'"; + + QgsDebugMsg( "sql: " + sql ); + + result = PQexec( sql ); + + if ( result.PQresultStatus() != PGRES_TUPLES_OK ) + { + QgsMessageLog::logMessage( tr( "Database connection was successful, but the accessible tables could not be determined.\nThe error message from the database was:\n%1\n" ) + .arg( result.PQresultErrorMessage() ), + tr( "PostGIS" ) ); + if ( nColumns == 0 ) + nColumns = -1; + } + else if ( result.PQntuples() > 0 ) + { + for ( int i = 0; i < result.PQntuples(); i++ ) + { + QString table = result.PQgetvalue( i, 0 ); // relname + QString schema = result.PQgetvalue( i, 1 ); // nspname + QString relkind = result.PQgetvalue( i, 2 ); // relation kind + + QgsDebugMsg( QString( "%1.%2: %3" ).arg( schema ).arg( table ).arg( relkind ) ); + + layerProperty.type = QString::null; + layerProperty.schemaName = schema; + layerProperty.tableName = table; + layerProperty.geometryColName = QString::null; + layerProperty.pkCols = relkind == "v" ? pkCandidates( schema, table ) : QStringList(); + layerProperty.srid = -1; + layerProperty.sql = ""; + + mLayersSupported << layerProperty; + nColumns++; + } + } + } + + if ( nColumns == 0 ) + { + QgsMessageLog::logMessage( tr( "Database connection was successful, but no accessible tables were found. Please verify that you have SELECT privilege on a table carrying PostGIS geometry." ), tr( "PostGIS" ) ); + } + + return nColumns > 0; +} + +bool QgsPostgresConn::supportedLayers( + QVector &layers, + bool searchGeometryColumnsOnly, + bool searchPublicOnly, + bool allowGeometrylessTables ) +{ + // Get the list of supported tables + if ( !getTableInfo( searchGeometryColumnsOnly, searchPublicOnly, allowGeometrylessTables ) ) + { + QgsMessageLog::logMessage( tr( "Unable to get list of spatially enabled tables from the database" ), tr( "PostGIS" ) ); + return false; + } + + layers = mLayersSupported; + + QgsDebugMsg( "Exiting." ); + + return true; +} + +/** + * Check to see if GEOS is available + */ +bool QgsPostgresConn::hasGEOS() +{ + // make sure info is up to date for the current connection + postgisVersion(); + // get geos capability + return mGeosAvailable; +} + +/** + * Check to see if topology is available + */ +bool QgsPostgresConn::hasTopology() +{ + // make sure info is up to date for the current connection + postgisVersion(); + // get topology capability + return mTopologyAvailable; +} + +/* Functions for determining available features in postGIS */ +QString QgsPostgresConn::postgisVersion() +{ + if ( mGotPostgisVersion ) + return mPostgisVersionInfo; + + mPostgresqlVersion = PQserverVersion( mConn ); + + QgsPostgresResult result = PQexec( "SELECT postgis_version()" ); + if ( result.PQntuples() != 1 ) + { + QgsMessageLog::logMessage( tr( "Retrieval of postgis version failed" ), tr( "PostGIS" ) ); + return QString::null; + } + + mPostgisVersionInfo = result.PQgetvalue( 0, 0 ); + + QgsDebugMsg( "PostGIS version info: " + mPostgisVersionInfo ); + + QStringList postgisParts = mPostgisVersionInfo.split( " ", QString::SkipEmptyParts ); + + // Get major and minor version + QStringList postgisVersionParts = postgisParts[0].split( ".", QString::SkipEmptyParts ); + if ( postgisVersionParts.size() < 2 ) + { + QgsMessageLog::logMessage( tr( "Could not parse postgis version string '%1'" ).arg( mPostgisVersionInfo ), tr( "PostGIS" ) ); + return QString::null; + } + + mPostgisVersionMajor = postgisVersionParts[0].toInt(); + mPostgisVersionMinor = postgisVersionParts[1].toInt(); + + mUseWkbHex = mPostgisVersionMajor < 1; + + // apparently postgis 1.5.2 doesn't report capabilities in postgis_version() anymore + if ( mPostgisVersionMajor > 1 || ( mPostgisVersionMajor == 1 && mPostgisVersionMinor >= 5 ) ) + { + result = PQexec( "SELECT postgis_geos_version(),postgis_proj_version()" ); + mGeosAvailable = result.PQntuples() == 1 && !result.PQgetisnull( 0, 0 ); + mProjAvailable = result.PQntuples() == 1 && !result.PQgetisnull( 0, 1 ); + QgsDebugMsg( QString( "geos:%1 proj:%2" ) + .arg( mGeosAvailable ? result.PQgetvalue( 0, 0 ) : "none" ) + .arg( mProjAvailable ? result.PQgetvalue( 0, 1 ) : "none" ) ); + mGistAvailable = true; + } + else + { + // assume no capabilities + mGeosAvailable = false; + mGistAvailable = false; + mProjAvailable = false; + + // parse out the capabilities and store them + QStringList geos = postgisParts.filter( "GEOS" ); + if ( geos.size() == 1 ) + { + mGeosAvailable = ( geos[0].indexOf( "=1" ) > -1 ); + } + QStringList gist = postgisParts.filter( "STATS" ); + if ( gist.size() == 1 ) + { + mGistAvailable = ( geos[0].indexOf( "=1" ) > -1 ); + } + QStringList proj = postgisParts.filter( "PROJ" ); + if ( proj.size() == 1 ) + { + mProjAvailable = ( proj[0].indexOf( "=1" ) > -1 ); + } + } + + // checking for topology support + QgsDebugMsg( "Checking for topology support" ); + mTopologyAvailable = false; + if ( mPostgisVersionMajor > 1 ) + { + QgsPostgresResult result = PQexec( "SELECT count(c.oid) FROM pg_class AS c JOIN pg_namespace AS n ON c.relnamespace=n.oid WHERE n.nspname='topology' AND c.relname='topology'" ); + if ( result.PQntuples() >= 1 ) + { + mTopologyAvailable = true; + } + } + + mGotPostgisVersion = true; + + return mPostgisVersionInfo; +} + +QString QgsPostgresConn::quotedIdentifier( QString ident ) +{ + ident.replace( '"', "\"\"" ); + return ident.prepend( "\"" ).append( "\"" ); +} + +QString QgsPostgresConn::quotedValue( QVariant value ) +{ + if ( value.isNull() ) + return "NULL"; + + switch ( value.type() ) + { + case QVariant::Int: + case QVariant::LongLong: + case QVariant::Double: + return value.toString(); + + default: + case QVariant::String: + QString v = value.toString(); + v.replace( "'", "''" ); + v.replace( "\\\"", "\\\\\"" ); + return v.prepend( "'" ).append( "'" ); + } +} + +PGresult *QgsPostgresConn::PQexec( QString query ) +{ + QgsDebugMsgLevel( QString( "Executing SQL: %1" ).arg( query ), 3 ); + PGresult *res = ::PQexec( mConn, query.toUtf8() ); + + if ( res ) + { + int errorStatus = PQresultStatus( res ); + if ( errorStatus != PGRES_COMMAND_OK && errorStatus != PGRES_TUPLES_OK ) + { + QgsMessageLog::logMessage( tr( "Errornous query: %1 returned %2 [%3]" ) + .arg( query ).arg( errorStatus ).arg( PQresultErrorMessage( res ) ), + tr( "PostGIS" ) ); + } + } + else + { + QgsMessageLog::logMessage( tr( "Query failed: %1\nError: %2" ).arg( query ), tr( "PostGIS" ) ); + } + + return res; +} + +bool QgsPostgresConn::openCursor( QString cursorName, QString sql ) +{ + if ( mOpenCursors++ == 0 ) + { + QgsDebugMsg( "Starting read-only transaction" ); + PQexecNR( "BEGIN READ ONLY" ); + } + QgsDebugMsgLevel( QString( "Binary cursor %1 for %2" ).arg( cursorName ).arg( sql ), 3 ); + return PQexecNR( QString( "DECLARE %1 BINARY CURSOR FOR %2" ).arg( cursorName ).arg( sql ) ); +} + +bool QgsPostgresConn::closeCursor( QString cursorName ) +{ + if ( !PQexecNR( QString( "CLOSE %1" ).arg( cursorName ) ) ) + return false; + + if ( --mOpenCursors == 0 ) + { + QgsDebugMsg( "Committing read-only transaction" ); + PQexecNR( "COMMIT" ); + } + + return true; +} + +bool QgsPostgresConn::PQexecNR( QString query, bool retry ) +{ + QgsPostgresResult res = ::PQexec( mConn, query.toUtf8() ); + + ExecStatusType errorStatus = res.PQresultStatus(); + if ( errorStatus == PGRES_COMMAND_OK ) + return true; + + QgsMessageLog::logMessage( tr( "Query: %1 returned %2 [%3]" ) + .arg( query ) + .arg( errorStatus ) + .arg( res.PQresultErrorMessage() ), + tr( "PostGIS" ) ); + + if ( mOpenCursors ) + { + QgsMessageLog::logMessage( tr( "%1 cursor states lost.\nSQL: %2\nResult: %3 (%4)" ) + .arg( mOpenCursors ).arg( query ).arg( errorStatus ) + .arg( res.PQresultErrorMessage() ), tr( "PostGIS" ) ); + mOpenCursors = 0; + } + + if ( PQstatus() == CONNECTION_OK ) + { + PQexecNR( "ROLLBACK" ); + } + else if ( retry ) + { + QgsMessageLog::logMessage( tr( "resetting bad connection." ), tr( "PostGIS" ) ); + ::PQreset( mConn ); + if ( PQstatus() == CONNECTION_OK ) + { + if ( PQexecNR( query, false ) ) + { + QgsMessageLog::logMessage( tr( "retry after reset succeeded." ), tr( "PostGIS" ) ); + return true; + } + else + { + QgsMessageLog::logMessage( tr( "retry after reset failed again." ), tr( "PostGIS" ) ); + return false; + } + } + else + { + QgsMessageLog::logMessage( tr( "connection still bad after reset." ), tr( "PostGIS" ) ); + } + } + else + { + QgsMessageLog::logMessage( tr( "bad connection, not retrying." ), tr( "PostGIS" ) ); + } + + return false; +} + +PGresult *QgsPostgresConn::PQgetResult() +{ + return ::PQgetResult( mConn ); +} + +PGresult *QgsPostgresConn::PQprepare( QString stmtName, QString query, int nParams, const Oid *paramTypes ) +{ + return ::PQprepare( mConn, stmtName.toUtf8(), query.toUtf8(), nParams, paramTypes ); +} + +PGresult *QgsPostgresConn::PQexecPrepared( QString stmtName, const QStringList ¶ms ) +{ + const char **param = new const char *[ params.size()]; + QList qparam; + + for ( int i = 0; i < params.size(); i++ ) + { + qparam << params[i].toUtf8(); + + if ( params[i].isNull() ) + param[i] = 0; + else + param[i] = qparam[i]; + } + + PGresult *res = ::PQexecPrepared( mConn, stmtName.toUtf8(), params.size(), param, NULL, NULL, 0 ); + + delete [] param; + + return res; +} + +void QgsPostgresConn::PQfinish() +{ + Q_ASSERT( mConn ); + ::PQfinish( mConn ); + mConn = 0; +} + +int QgsPostgresConn::PQstatus() +{ + Q_ASSERT( mConn ); + return ::PQstatus( mConn ); +} + +QString QgsPostgresConn::PQerrorMessage() +{ + Q_ASSERT( mConn ); + return QString::fromUtf8( ::PQerrorMessage( mConn ) ); +} + +int QgsPostgresConn::PQsendQuery( QString query ) +{ + return ::PQsendQuery( mConn, query.toUtf8() ); +} + +qint64 QgsPostgresConn::getBinaryInt( QgsPostgresResult &queryResult, int row, int col ) +{ + qint64 oid; + char *p = PQgetvalue( queryResult.result(), row, col ); + size_t s = PQgetlength( queryResult.result(), row, col ); + +#ifdef QGISDEBUG + QString buf = ""; + for ( size_t i = 0; i < s; i++ ) + { + buf += QString( "%1 " ).arg( *( unsigned char * )( p + i ), 0, 16, QLatin1Char( ' ' ) ); + } + + QgsDebugMsgLevel( QString( "int in hex:%1" ).arg( buf ), 4 ); +#endif + + switch ( s ) + { + case 2: + oid = *( qint16 * )p; + if ( mSwapEndian ) + oid = ntohs( oid ); + break; + + case 6: + { + qint64 block = *( qint32 * ) p; + qint64 offset = *( qint16 * )( p + sizeof( qint32 ) ); + + if ( mSwapEndian ) + { + block = ntohl( block ); + offset = ntohs( offset ); + } + + oid = ( block << 16 ) + offset; + } + break; + + case 8: + { + qint32 oid0 = *( qint32 * ) p; + qint32 oid1 = *( qint32 * )( p + sizeof( qint32 ) ); + + if ( mSwapEndian ) + { + QgsDebugMsgLevel( QString( "swap oid0:%1 oid1:%2" ).arg( oid0 ).arg( oid1 ), 4 ); + oid0 = ntohl( oid0 ); + oid1 = ntohl( oid1 ); + } + + QgsDebugMsgLevel( QString( "oid0:%1 oid1:%2" ).arg( oid0 ).arg( oid1 ), 4 ); + oid = oid0; + QgsDebugMsgLevel( QString( "oid:%1" ).arg( oid ), 4 ); + oid <<= 32; + QgsDebugMsgLevel( QString( "oid:%1" ).arg( oid ), 4 ); + oid |= oid1; + QgsDebugMsgLevel( QString( "oid:%1" ).arg( oid ), 4 ); + } + break; + + default: + QgsDebugMsg( QString( "unexpected size %1" ).arg( s ) ); + + case 4: + oid = *( qint32 * )p; + if ( mSwapEndian ) + oid = ntohl( oid ); + break; + } + + return oid; +} + +QString QgsPostgresConn::fieldExpression( const QgsField &fld ) +{ + const QString &type = fld.typeName(); + if ( type == "money" ) + { + return QString( "cash_out(%1)" ).arg( quotedIdentifier( fld.name() ) ); + } + else if ( type.startsWith( "_" ) ) + { + return QString( "array_out(%1)" ).arg( quotedIdentifier( fld.name() ) ); + } + else if ( type == "bool" ) + { + return QString( "boolout(%1)" ).arg( quotedIdentifier( fld.name() ) ); + } + else if ( type == "geometry" ) + { + return QString( "%1(%2)" ) + .arg( majorVersion() < 2 ? "asewkt" : "st_asewkt" ) + .arg( quotedIdentifier( fld.name() ) ); + } + else if ( type == "geography" ) + { + return QString( "st_astext(%1)" ).arg( quotedIdentifier( fld.name() ) ); + } + else + { + return quotedIdentifier( fld.name() ) + "::text"; + } +} + +void QgsPostgresConn::deduceEndian() +{ + // need to store the PostgreSQL endian format used in binary cursors + // since it appears that starting with + // version 7.4, binary cursors return data in XDR whereas previous versions + // return data in the endian of the server + + QgsPostgresResult res = PQexec( "select regclass('pg_class')::oid" ); + QString oidValue = res.PQgetvalue( 0, 0 ); + + QgsDebugMsg( "Creating binary cursor" ); + + // get the same value using a binary cursor + openCursor( "oidcursor", "select regclass('pg_class')::oid" ); + + QgsDebugMsg( "Fetching a record and attempting to get check endian-ness" ); + + res = PQexec( "fetch forward 1 from oidcursor" ); + + mSwapEndian = true; + if ( res.PQntuples() > 0 ) + { + // get the oid value from the binary cursor + qint64 oid = getBinaryInt( res, 0, 0 ); + + QgsDebugMsg( QString( "Got oid of %1 from the binary cursor" ).arg( oid ) ); + QgsDebugMsg( QString( "First oid is %1" ).arg( oidValue ) ); + + // compare the two oid values to determine if we need to do an endian swap + if ( oid != oidValue.toLongLong() ) + mSwapEndian = false; + } + + closeCursor( "oidcursor" ); +} + +void QgsPostgresConn::retrieveLayerTypes( QgsPostgresLayerProperty &layerProperty, bool useEstimatedMetadata ) +{ + QString table; + + // it is possible that the where clause restricts the feature type or srid + if ( useEstimatedMetadata ) + { + table = QString( "(SELECT %1 FROM %2.%3 WHERE %1 IS NOT NULL%4 LIMIT %5) AS t" ) + .arg( quotedIdentifier( layerProperty.geometryColName ) ) + .arg( quotedIdentifier( layerProperty.schemaName ) ) + .arg( quotedIdentifier( layerProperty.tableName ) ) + .arg( layerProperty.sql.isEmpty() ? "" : QString( " AND (%1)" ).arg( layerProperty.sql ) ) + .arg( sGeomTypeSelectLimit ); + } + else + { + table = QString( "%1.%2%3" ) + .arg( quotedIdentifier( layerProperty.schemaName ) ) + .arg( quotedIdentifier( layerProperty.tableName ) ) + .arg( layerProperty.sql.isEmpty() ? "" : QString( " WHERE %1" ).arg( layerProperty.sql ) ); + } + + QString query = QString( "SELECT DISTINCT" + " CASE" + " WHEN upper(geometrytype(%1)) IN ('POINT','MULTIPOINT') THEN 'POINT'" + " WHEN upper(geometrytype(%1)) IN ('LINESTRING','MULTILINESTRING') THEN 'LINESTRING'" + " WHEN upper(geometrytype(%1)) IN ('POLYGON','MULTIPOLYGON') THEN 'POLYGON'" + " END," + " coalesce(%2(%1),-1)" + " FROM %3" ) + .arg( quotedIdentifier( layerProperty.geometryColName ) ) + .arg( majorVersion() < 2 ? "srid" : "st_srid" ) + .arg( table ); + + QgsDebugMsg( "Retrieving geometry types: " + query ); + + QgsPostgresResult gresult = PQexec( query ); + + QString type; + int srid = -1; + if ( gresult.PQresultStatus() == PGRES_TUPLES_OK ) + { + QStringList types; + + for ( int i = 0; i < gresult.PQntuples(); i++ ) + { + QString type = gresult.PQgetvalue( i, 0 ); + if ( !type.isEmpty() ) + types << type; + srid = gresult.PQgetvalue( i, 1 ).toInt(); + } + + type = types.join( "," ); + } + + QgsDebugMsg( QString( "type:%1 srid:%2" ).arg( type ).arg( srid ) ); + layerProperty.type = type; + layerProperty.srid = srid; +} + +void QgsPostgresConn::postgisGeometryType( QGis::WkbType wkbType, QString &geometryType, int &dim ) +{ + switch ( wkbType ) + { + case QGis::WKBPoint25D: + dim = 3; + case QGis::WKBPoint: + geometryType = "POINT"; + break; + + case QGis::WKBLineString25D: + dim = 3; + case QGis::WKBLineString: + geometryType = "LINESTRING"; + break; + + case QGis::WKBPolygon25D: + dim = 3; + case QGis::WKBPolygon: + geometryType = "POLYGON"; + break; + + case QGis::WKBMultiPoint25D: + dim = 3; + case QGis::WKBMultiPoint: + geometryType = "MULTIPOINT"; + break; + + case QGis::WKBMultiLineString25D: + dim = 3; + case QGis::WKBMultiLineString: + geometryType = "MULTILINESTRING"; + break; + + case QGis::WKBMultiPolygon25D: + dim = 3; + case QGis::WKBMultiPolygon: + geometryType = "MULTIPOLYGON"; + break; + + case QGis::WKBUnknown: + geometryType = "GEOMETRY"; + break; + + case QGis::WKBNoGeometry: + default: + dim = 0; + break; + } +} + +QString QgsPostgresConn::postgisGeometryTypeName( QGis::WkbType wkbType ) +{ + QString geometryType; + int dim; + + postgisGeometryType( wkbType, geometryType, dim ); + + return geometryType; +} + +int QgsPostgresConn::postgisGeometryTypeDim( QGis::WkbType wkbType ) +{ + QString geometryType; + int dim; + + postgisGeometryType( wkbType, geometryType, dim ); + + return dim; +} + +QGis::WkbType QgsPostgresConn::wkbTypeFromPostgis( QString type ) +{ + if ( type == "POINT" || type == "POINTM" ) + { + return QGis::WKBPoint; + } + else if ( type == "MULTIPOINT" || type == "MULTIPOINTM" ) + { + return QGis::WKBMultiPoint; + } + else if ( type == "LINESTRING" || type == "LINESTRINGM" ) + { + return QGis::WKBLineString; + } + else if ( type == "MULTILINESTRING" || type == "MULTILINESTRINGM" ) + { + return QGis::WKBMultiLineString; + } + else if ( type == "POLYGON" || type == "POLYGONM" ) + { + return QGis::WKBPolygon; + } + else if ( type == "MULTIPOLYGON" || type == "MULTIPOLYGONM" ) + { + return QGis::WKBMultiPolygon; + } + else + { + return QGis::WKBUnknown; + } +} + +QStringList QgsPostgresConn::connectionList() +{ + QSettings settings; + settings.beginGroup( "/PostgreSQL/connections" ); + return settings.childGroups(); +} + +QString QgsPostgresConn::selectedConnection() +{ + QSettings settings; + return settings.value( "/PostgreSQL/connections/selected" ).toString(); +} + +void QgsPostgresConn::setSelectedConnection( QString name ) +{ + QSettings settings; + return settings.setValue( "/PostgreSQL/connections/selected", name ); +} + +QgsDataSourceURI QgsPostgresConn::connUri( QString theConnName ) +{ + QgsDebugMsg( "theConnName = " + theConnName ); + + QSettings settings; + + QString key = "/PostgreSQL/connections/" + theConnName; + + QString service = settings.value( key + "/service" ).toString(); + QString host = settings.value( key + "/host" ).toString(); + QString port = settings.value( key + "/port" ).toString(); + if ( port.length() == 0 ) + { + port = "5432"; + } + QString database = settings.value( key + "/database" ).toString(); + + //bool publicSchemaOnly = settings.value( key + "/publicOnly", false ).toBool(); + //bool geometryColumnsOnly = settings.value( key + "/geometrycolumnsOnly", false ).toBool(); + //bool allowGeometrylessTables = settings.value( key + "/allowGeometrylessTables", false ).toBool(); + + bool useEstimatedMetadata = settings.value( key + "/estimatedMetadata", false ).toBool(); + int sslmode = settings.value( key + "/sslmode", QgsDataSourceURI::SSLprefer ).toInt(); + + QString username; + QString password; + if ( settings.value( key + "/saveUsername" ).toString() == "true" ) + { + username = settings.value( key + "/username" ).toString(); + } + + if ( settings.value( key + "/savePassword" ).toString() == "true" ) + { + password = settings.value( key + "/password" ).toString(); + } + + // Old save setting + if ( settings.contains( key + "/save" ) ) + { + username = settings.value( key + "/username" ).toString(); + + if ( settings.value( key + "/save" ).toString() == "true" ) + { + password = settings.value( key + "/password" ).toString(); + } + } + + QgsDataSourceURI uri; + if ( !service.isEmpty() ) + { + uri.setConnection( service, database, username, password, ( QgsDataSourceURI::SSLmode ) sslmode ); + } + else + { + uri.setConnection( host, port, database, username, password, ( QgsDataSourceURI::SSLmode ) sslmode ); + } + uri.setUseEstimatedMetadata( useEstimatedMetadata ); + + return uri; +} diff --git a/src/providers/postgres/qgspostgresconn.h b/src/providers/postgres/qgspostgresconn.h new file mode 100644 index 00000000000..12a73be3c8b --- /dev/null +++ b/src/providers/postgres/qgspostgresconn.h @@ -0,0 +1,236 @@ +/*************************************************************************** + qgspostgresconn.h - connection class to PostgreSQL/PostGIS + ------------------- + begin : 2011/01/28 + copyright : (C) 2011 by Juergen E. Fischer + email : jef at norbit dot de + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef QGSPOSTGRESCONN_H +#define QGSPOSTGRESCONN_H + +#include +#include +#include +#include +#include + +#include "qgis.h" +#include "qgsdatasourceuri.h" + +extern "C" +{ +#include +} + +class QgsField; + +/** Layer Property structure */ +// TODO: Fill to Postgres/PostGIS specifications +struct QgsPostgresLayerProperty +{ + // Postgres/PostGIS layer properties + QString type; + QString schemaName; + QString tableName; + QString geometryColName; + QStringList pkCols; + int srid; + QString sql; +}; + +class QgsPostgresResult +{ + public: + QgsPostgresResult( PGresult *theRes = 0 ) : mRes( theRes ) {} + ~QgsPostgresResult(); + + QgsPostgresResult &operator=( PGresult *theRes ); + QgsPostgresResult &operator=( const QgsPostgresResult &src ); + + ExecStatusType PQresultStatus(); + QString PQresultErrorMessage(); + + int PQntuples(); + QString PQgetvalue( int row, int col ); + bool PQgetisnull( int row, int col ); + + int PQnfields(); + QString PQfname( int col ); + int PQftable( int col ); + int PQftype( int col ); + int PQftablecol( int col ); + + PGresult *result() const { return mRes; } + + private: + PGresult *mRes; +}; + +class QgsPostgresConn : public QObject +{ + Q_OBJECT; + public: + static QgsPostgresConn *connectDb( QString connInfo, bool readOnly ); + void disconnect(); + + //! get postgis version string + QString postgisVersion(); + + //! get status of GEOS capability + bool hasGEOS(); + + //! get status of topology capability + bool hasTopology(); + + //! get status of GIST capability + bool hasGIST(); + + //! get status of PROJ4 capability + bool hasPROJ(); + + //! encode wkb in hex + bool useWkbHex() { return mUseWkbHex; } + + //! major PostgreSQL version + int majorVersion() { return mPostgisVersionMajor; } + + //! PostgreSQL version + int pgVersion() { return mPostgresqlVersion; } + + //! run a query and free result buffer + bool PQexecNR( QString query, bool retry = true ); + + //! cursor handling + bool openCursor( QString cursorName, QString declare ); + bool closeCursor( QString cursorName ); + +#if 0 + PGconn *pgConnection() { return mConn; } +#endif + + // + // libpq wrapper + // + + // run a query and check for errors + PGresult *PQexec( QString query ); + void PQfinish(); + QString PQerrorMessage(); + int PQsendQuery( QString query ); + int PQstatus(); + PGresult *PQgetResult(); + PGresult *PQprepare( QString stmtName, QString query, int nParams, const Oid *paramTypes ); + PGresult *PQexecPrepared( QString stmtName, const QStringList ¶ms ); + + /** Double quote a PostgreSQL identifier for placement in a SQL string. + */ + static QString quotedIdentifier( QString ident ); + + /** Quote a value for placement in a SQL string. + */ + static QString quotedValue( QVariant value ); + + //! Get the list of supported layers + bool supportedLayers( QVector &layers, + bool searchGeometryColumnsOnly = true, + bool searchPublicOnly = true, + bool allowGeometrylessTables = false ); + + void retrieveLayerTypes( QgsPostgresLayerProperty &layerProperty, bool useEstimatedMetadata ); + + /** Gets information about the spatial tables */ + bool getTableInfo( bool searchGeometryColumnsOnly, bool searchPublicOnly, bool allowGeometrylessTables ); + + /** get primary key candidates (all int4 columns) */ + QStringList pkCandidates( QString schemaName, QString viewName ); + + qint64 getBinaryInt( QgsPostgresResult &queryResult, int row, int col ); + + QString fieldExpression( const QgsField &fld ); + + QString connInfo() const { return mConnInfo; } + + static const int sGeomTypeSelectLimit; + + static QString postgisGeometryTypeName( QGis::WkbType wkbType ); + static int postgisGeometryTypeDim( QGis::WkbType wkbType ); + static void postgisGeometryType( QGis::WkbType wkbType, QString &geometryType, int &dim ); + static QGis::WkbType wkbTypeFromPostgis( QString type ); + + static QStringList connectionList(); + static QString selectedConnection(); + static void setSelectedConnection( QString theConnName ); + static QgsDataSourceURI connUri( QString theConnName ); + + private: + QgsPostgresConn( QString conninfo, bool readOnly ); + ~QgsPostgresConn(); + + int mRef; + int mOpenCursors; + PGconn *mConn; + QString mConnInfo; + + //! GEOS capability + bool mGeosAvailable; + + //! Topology capability + bool mTopologyAvailable; + + //! PostGIS version string + QString mPostgisVersionInfo; + + //! Are mPostgisVersionMajor, mPostgisVersionMinor, mGeosAvailable, mGistAvailable, mProjAvailable, mTopologyAvailable valid? + bool mGotPostgisVersion; + + //! PostgreSQL version + int mPostgresqlVersion; + + //! PostGIS major version + int mPostgisVersionMajor; + + //! PostGIS minor version + int mPostgisVersionMinor; + + //! GIST capability + bool mGistAvailable; + + //! PROJ4 capability + bool mProjAvailable; + + //! encode wkb in hex + bool mUseWkbHex; + + bool mReadOnly; + + static QMap sConnectionsRW; + static QMap sConnectionsRO; + + //! List of the supported layers + QVector mLayersSupported; + + /** + * Flag indicating whether data from binary cursors must undergo an + * endian conversion prior to use + @note + + XXX Umm, it'd be helpful to know what we're swapping from and to. + XXX Presumably this means swapping from big-endian (network) byte order + XXX to little-endian; but the inverse transaction is possible, too, and + XXX that's not reflected in this variable + */ + bool mSwapEndian; + void deduceEndian(); +}; + +#endif diff --git a/src/providers/postgres/qgspostgresconnection.cpp b/src/providers/postgres/qgspostgresconnection.cpp deleted file mode 100644 index 6695fca7b35..00000000000 --- a/src/providers/postgres/qgspostgresconnection.cpp +++ /dev/null @@ -1,132 +0,0 @@ -/*************************************************************************** - qgspostgresconnection.cpp - PostgreSQL/PostGIS connection - ------------------- - begin : 3 June 2011 - copyright : (C) 2011 by Giuseppe Sucameli - email : brush.tyler at gmail dot 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. * - * * - ***************************************************************************/ - -#include "qgspostgresconnection.h" - -#include -#include "qgspostgresprovider.h" -#include "qgsproviderregistry.h" -#include "qgsdatasourceuri.h" - -#include - -QStringList QgsPostgresConnection::connectionList() -{ - QSettings settings; - settings.beginGroup( "/PostgreSQL/connections" ); - return settings.childGroups(); -} - -QString QgsPostgresConnection::selectedConnection() -{ - QSettings settings; - return settings.value( "/PostgreSQL/connections/selected" ).toString(); -} - -void QgsPostgresConnection::setSelectedConnection( QString name ) -{ - QSettings settings; - return settings.setValue( "/PostgreSQL/connections/selected", name ); -} - - -QgsPostgresConnection::QgsPostgresConnection( QString theConnName ) : - mConnName( theConnName ) -{ - QgsDebugMsg( "theConnName = " + theConnName ); - - QSettings settings; - - QString key = "/PostgreSQL/connections/" + mConnName; - - QString service = settings.value( key + "/service" ).toString(); - QString host = settings.value( key + "/host" ).toString(); - QString port = settings.value( key + "/port" ).toString(); - if ( port.length() == 0 ) - { - port = "5432"; - } - QString database = settings.value( key + "/database" ).toString(); - - //bool publicSchemaOnly = settings.value( key + "/publicOnly", false ).toBool(); - //bool geometryColumnsOnly = settings.value( key + "/geometrycolumnsOnly", false ).toBool(); - //bool allowGeometrylessTables = settings.value( key + "/allowGeometrylessTables", false ).toBool(); - - bool useEstimatedMetadata = settings.value( key + "/estimatedMetadata", false ).toBool(); - int sslmode = settings.value( key + "/sslmode", QgsDataSourceURI::SSLprefer ).toInt(); - - QString username; - QString password; - if ( settings.value( key + "/saveUsername" ).toString() == "true" ) - { - username = settings.value( key + "/username" ).toString(); - } - - if ( settings.value( key + "/savePassword" ).toString() == "true" ) - { - password = settings.value( key + "/password" ).toString(); - } - - // Old save setting - if ( settings.contains( key + "/save" ) ) - { - username = settings.value( key + "/username" ).toString(); - - if ( settings.value( key + "/save" ).toString() == "true" ) - { - password = settings.value( key + "/password" ).toString(); - } - } - - QgsDataSourceURI uri; - if ( !service.isEmpty() ) - { - uri.setConnection( service, database, username, password, ( QgsDataSourceURI::SSLmode ) sslmode ); - } - else - { - uri.setConnection( host, port, database, username, password, ( QgsDataSourceURI::SSLmode ) sslmode ); - } - uri.setUseEstimatedMetadata( useEstimatedMetadata ); - mConnectionInfo = uri.uri(); - - QgsDebugMsg( QString( "Connection info: '%1'." ).arg( mConnectionInfo ) ); -} - -QgsPostgresConnection::~QgsPostgresConnection() -{ - -} - -QString QgsPostgresConnection::connectionInfo( ) -{ - return mConnectionInfo; -} - -QgsPostgresProvider * QgsPostgresConnection::provider( ) -{ - // TODO: Create and bind to data provider - - // load the server data provider plugin - QgsProviderRegistry * pReg = QgsProviderRegistry::instance(); - - QgsPostgresProvider *postgresProvider = - ( QgsPostgresProvider* ) pReg->provider( "postgres", mConnectionInfo ); - - return postgresProvider; -} - diff --git a/src/providers/postgres/qgspostgresconnection.h b/src/providers/postgres/qgspostgresconnection.h deleted file mode 100644 index 16dfae029f4..00000000000 --- a/src/providers/postgres/qgspostgresconnection.h +++ /dev/null @@ -1,51 +0,0 @@ -/*************************************************************************** - qgspostgresconnection.h - PostgreSQL/PostGIS connection - ------------------- - begin : 3 June 2011 - copyright : (C) 2011 by Giuseppe Sucameli - email : brush.tyler at gmail dot 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. * - * * - ***************************************************************************/ - -#ifndef QGSPOSTGRESCONNECTION_H -#define QGSPOSTGRESCONNECTION_H - -#include - -class QgsPostgresProvider; - -/*! - * \brief Connections management - */ -class QgsPostgresConnection : public QObject -{ - Q_OBJECT - - public: - //! Constructor - QgsPostgresConnection( QString theConnName ); - //! Destructor - ~QgsPostgresConnection(); - - static QStringList connectionList(); - - static QString selectedConnection(); - static void setSelectedConnection( QString name ); - - public: - QgsPostgresProvider *provider(); - QString connectionInfo(); - QString mConnName; - QString mConnectionInfo; -}; - - -#endif // QGSPOSTGRESCONNECTION_H diff --git a/src/providers/postgres/qgspostgresdataitems.cpp b/src/providers/postgres/qgspostgresdataitems.cpp index e0aab8d8b20..3fcb330bbbc 100644 --- a/src/providers/postgres/qgspostgresdataitems.cpp +++ b/src/providers/postgres/qgspostgresdataitems.cpp @@ -1,10 +1,10 @@ #include "qgspostgresdataitems.h" -#include "qgslogger.h" - -#include "qgspostgresconnection.h" #include "qgspgsourceselect.h" #include "qgspgnewconnection.h" +#include "qgscolumntypethread.h" +#include "qgslogger.h" +#include "qgsdatasourceuri.h" // --------------------------------------------------------------------------- QgsPGConnectionItem::QgsPGConnectionItem( QgsDataItem* parent, QString name, QString path ) @@ -21,47 +21,87 @@ QVector QgsPGConnectionItem::createChildren() { QgsDebugMsg( "Entered" ); QVector children; - QgsPostgresConnection connection( mName ); - QgsPostgresProvider *pgProvider = connection.provider( ); - if ( !pgProvider ) + QgsDataSourceURI uri = QgsPostgresConn::connUri( mName ); + + mConn = QgsPostgresConn::connectDb( uri.connectionInfo(), true ); + if ( !mConn ) return children; - QString mConnInfo = connection.connectionInfo(); - QgsDebugMsg( "mConnInfo = " + mConnInfo ); - QVector layerProperties; - if ( !pgProvider->supportedLayers( layerProperties, true, false, false ) ) + if ( !mConn->supportedLayers( layerProperties, true, false, false ) ) { children.append( new QgsErrorItem( this, tr( "Failed to retrieve layers" ), mPath + "/error" ) ); return children; } - // fill the schemas map - mSchemasMap.clear(); + QgsGeomColumnTypeThread *columnTypeThread = 0; + foreach( QgsPostgresLayerProperty layerProperty, layerProperties ) { - mSchemasMap[ layerProperty.schemaName ].push_back( layerProperty ); + QgsPGSchemaItem *schemaItem = mSchemaMap.value( layerProperty.schemaName, 0 ); + if ( !schemaItem ) + { + schemaItem = new QgsPGSchemaItem( this, layerProperty.schemaName, mPath + "/" + layerProperty.schemaName ); + children.append( schemaItem ); + mSchemaMap[ layerProperty.schemaName ] = schemaItem; + } + + if ( layerProperty.type == "GEOMETRY" ) + { + if ( !columnTypeThread ) + { + QgsPostgresConn *conn = QgsPostgresConn::connectDb( uri.connectionInfo(), true /* readonly */ ); + if ( conn ) + { + columnTypeThread = new QgsGeomColumnTypeThread( conn, true /* use estimated metadata */ ); + + connect( columnTypeThread, SIGNAL( setLayerType( QgsPostgresLayerProperty ) ), + this, SLOT( setLayerType( QgsPostgresLayerProperty ) ) ); + columnTypeThread->start(); + } + } + columnTypeThread->addGeometryColumn( layerProperty ); + continue; + } + + schemaItem->addLayer( layerProperty ); } - QMap >::const_iterator it = mSchemasMap.constBegin(); - for ( ; it != mSchemasMap.constEnd(); it++ ) - { - QgsDebugMsg( "schema: " + it.key() ); - QgsPGSchemaItem * schema = new QgsPGSchemaItem( this, it.key(), mPath + "/" + it.key(), mConnInfo ); - - children.append( schema ); - } return children; } +void QgsPGConnectionItem::setLayerType( QgsPostgresLayerProperty layerProperty ) +{ + QgsPGSchemaItem *schemaItem = mSchemaMap.value( layerProperty.schemaName, 0 ); + + if ( !schemaItem ) + { + QgsDebugMsg( QString( "schema item for %1 not found." ).arg( layerProperty.schemaName ) ); + return; + } + + foreach( QString type, layerProperty.type.split( ",", QString::SkipEmptyParts ) ) + { + QGis::WkbType wkbType = QgsPgTableModel::qgisTypeFromDbType( type ); + if ( wkbType == QGis::WKBUnknown ) + { + QgsDebugMsg( QString( "unsupported geometry type:%1" ).arg( type ) ); + continue; + } + + schemaItem->addLayer( layerProperty ); + } +} + bool QgsPGConnectionItem::equal( const QgsDataItem *other ) { if ( type() != other->type() ) { return false; } - const QgsPGConnectionItem *o = dynamic_cast( other ); - return ( mPath == o->mPath && mName == o->mName && mConnInfo == o->mConnInfo ); + + const QgsPGConnectionItem *o = qobject_cast( other ); + return ( mPath == o->mPath && mName == o->mName && o->connection() == connection() ); } QList QgsPGConnectionItem::actions() @@ -98,10 +138,9 @@ void QgsPGConnectionItem::deleteConnection() // --------------------------------------------------------------------------- -QgsPGLayerItem::QgsPGLayerItem( QgsDataItem* parent, QString name, QString path, QString connInfo, QgsLayerItem::LayerType layerType, QgsPostgresLayerProperty layerProperty ) - : QgsLayerItem( parent, name, path, QString(), layerType, "postgres" ), - mConnInfo( connInfo ), - mLayerProperty( layerProperty ) +QgsPGLayerItem::QgsPGLayerItem( QgsDataItem* parent, QString name, QString path, QgsLayerItem::LayerType layerType, QgsPostgresLayerProperty layerProperty ) + : QgsLayerItem( parent, name, path, QString(), layerType, "postgres" ) + , mLayerProperty( layerProperty ) { mUri = createUri(); mPopulated = true; @@ -114,63 +153,75 @@ QgsPGLayerItem::~QgsPGLayerItem() QString QgsPGLayerItem::createUri() { QString pkColName = mLayerProperty.pkCols.size() > 0 ? mLayerProperty.pkCols.at( 0 ) : QString::null; - QgsDataSourceURI uri( mConnInfo ); + QgsPGConnectionItem *connItem = qobject_cast( parent() ? parent()->parent() : 0 ); + + if ( !connItem ) + { + QgsDebugMsg( "connection item not found." ); + return QString::null; + } + + QgsDebugMsg( QString( "connInfo: %1" ).arg( connItem->connection()->connInfo() ) ); + + QgsDataSourceURI uri( connItem->connection()->connInfo() ); uri.setDataSource( mLayerProperty.schemaName, mLayerProperty.tableName, mLayerProperty.geometryColName, mLayerProperty.sql, pkColName ); + uri.setSrid( QString::number( mLayerProperty.srid ) ); + uri.setGeometryType( QgsPgTableModel::qgisTypeFromDbType( mLayerProperty.type ) ); + QgsDebugMsg( QString( "layer uri: %1" ).arg( uri.uri() ) ); return uri.uri(); } // --------------------------------------------------------------------------- -QgsPGSchemaItem::QgsPGSchemaItem( QgsDataItem* parent, QString name, QString path, QString connInfo ) +QgsPGSchemaItem::QgsPGSchemaItem( QgsDataItem* parent, QString name, QString path ) : QgsDataCollectionItem( parent, name, path ) { mIcon = QIcon( getThemePixmap( "mIconDbSchema.png" ) ); - mConnInfo = connInfo; } QVector QgsPGSchemaItem::createChildren() { - QgsPGConnectionItem* connItem = dynamic_cast( mParent ); - Q_ASSERT( connItem ); - QVector layers = connItem->mSchemasMap.value( mName ); - - QVector children; - // Populate everything, it costs nothing, all info about layers is collected - foreach( QgsPostgresLayerProperty layerProperty, layers ) - { - QgsDebugMsg( "table: " + layerProperty.schemaName + "." + layerProperty.tableName ); - - QgsLayerItem::LayerType layerType = QgsLayerItem::NoType; - if ( layerProperty.type.contains( "POINT" ) ) - { - layerType = QgsLayerItem::Point; - } - else if ( layerProperty.type.contains( "LINE" ) ) - { - layerType = QgsLayerItem::Line; - } - else if ( layerProperty.type.contains( "POLYGON" ) ) - { - layerType = QgsLayerItem::Polygon; - } - else if ( layerProperty.type == QString::null ) - { - if ( layerProperty.geometryColName == QString::null ) - { - layerType = QgsLayerItem::TableLayer; - } - } - - QgsPGLayerItem * layer = new QgsPGLayerItem( this, layerProperty.tableName, mPath + "/" + layerProperty.tableName, mConnInfo, layerType, layerProperty ); - children.append( layer ); - } - - return children; + QgsDebugMsg( "Entering." ); + return QVector(); } QgsPGSchemaItem::~QgsPGSchemaItem() { } +void QgsPGSchemaItem::addLayer( QgsPostgresLayerProperty layerProperty ) +{ + QGis::WkbType wkbType = QgsPgTableModel::qgisTypeFromDbType( layerProperty.type ); + QString name = layerProperty.tableName + "." + layerProperty.geometryColName; + + QgsLayerItem::LayerType layerType; + if ( layerProperty.type.contains( "POINT" ) ) + { + layerType = QgsLayerItem::Point; + } + else if ( layerProperty.type.contains( "LINE" ) ) + { + layerType = QgsLayerItem::Line; + } + else if ( layerProperty.type.contains( "POLYGON" ) ) + { + layerType = QgsLayerItem::Polygon; + } + else if ( layerProperty.type.isEmpty() && layerProperty.geometryColName.isEmpty() ) + { + layerType = QgsLayerItem::TableLayer; + name = layerProperty.tableName; + } + else + { + return; + } + + name += " (" + QgsPgTableModel::displayStringForType( wkbType ) + ")"; + + QgsPGLayerItem *layerItem = new QgsPGLayerItem( this, name, mPath + "/" + name, layerType, layerProperty ); + addChild( layerItem ); +} + // --------------------------------------------------------------------------- QgsPGRootItem::QgsPGRootItem( QgsDataItem* parent, QString name, QString path ) : QgsDataCollectionItem( parent, name, path ) @@ -183,13 +234,12 @@ QgsPGRootItem::~QgsPGRootItem() { } -QVectorQgsPGRootItem::createChildren() +QVector QgsPGRootItem::createChildren() { QVector connections; - foreach( QString connName, QgsPostgresConnection::connectionList() ) + foreach( QString connName, QgsPostgresConn::connectionList() ) { - QgsDataItem * conn = new QgsPGConnectionItem( this, connName, mPath + "/" + connName ); - connections.push_back( conn ); + connections << new QgsPGConnectionItem( this, connName, mPath + "/" + connName ); } return connections; } @@ -205,12 +255,13 @@ QList QgsPGRootItem::actions() return lst; } -QWidget * QgsPGRootItem::paramWidget() +QWidget *QgsPGRootItem::paramWidget() { QgsPgSourceSelect *select = new QgsPgSourceSelect( 0, 0, true, true ); connect( select, SIGNAL( connectionsChanged() ), this, SLOT( connectionsChanged() ) ); return select; } + void QgsPGRootItem::connectionsChanged() { refresh(); @@ -224,22 +275,3 @@ void QgsPGRootItem::newConnection() refresh(); } } - -// --------------------------------------------------------------------------- - -QGISEXTERN QgsPgSourceSelect * selectWidget( QWidget * parent, Qt::WFlags fl ) -{ - // TODO: this should be somewhere else - return new QgsPgSourceSelect( parent, fl ); -} - -QGISEXTERN int dataCapabilities() -{ - return QgsDataProvider::Database; -} - -QGISEXTERN QgsDataItem * dataItem( QString thePath, QgsDataItem* parentItem ) -{ - Q_UNUSED( thePath ); - return new QgsPGRootItem( parentItem, "PostGIS", "pg:" ); -} diff --git a/src/providers/postgres/qgspostgresdataitems.h b/src/providers/postgres/qgspostgresdataitems.h index f8c442190ee..7df0dd7a50c 100644 --- a/src/providers/postgres/qgspostgresdataitems.h +++ b/src/providers/postgres/qgspostgresdataitems.h @@ -3,57 +3,13 @@ #include "qgsdataitem.h" -#include "qgspostgresprovider.h" +#include "qgspostgresconn.h" +#include "qgspgsourceselect.h" -class QgsPGConnectionItem : public QgsDataCollectionItem -{ - Q_OBJECT - public: - QgsPGConnectionItem( QgsDataItem* parent, QString name, QString path ); - ~QgsPGConnectionItem(); - - QVector createChildren(); - virtual bool equal( const QgsDataItem *other ); - - virtual QList actions(); - - QString mConnInfo; - QMap > mSchemasMap; - - public slots: - void editConnection(); - void deleteConnection(); -}; - -// WMS Layers may be nested, so that they may be both QgsDataCollectionItem and QgsLayerItem -// We have to use QgsDataCollectionItem and support layer methods if necessary -class QgsPGLayerItem : public QgsLayerItem -{ - Q_OBJECT - public: - QgsPGLayerItem( QgsDataItem* parent, QString name, QString path, - QString connInfo, QgsLayerItem::LayerType layerType, QgsPostgresLayerProperty layerProperties ); - ~QgsPGLayerItem(); - - QString createUri(); - - QString mConnInfo; - QgsPostgresLayerProperty mLayerProperty; -}; - -class QgsPGSchemaItem : public QgsDataCollectionItem -{ - Q_OBJECT - public: - QgsPGSchemaItem( QgsDataItem* parent, QString name, QString path, - QString connInfo ); - ~QgsPGSchemaItem(); - - QVector createChildren(); - - protected: - QString mConnInfo; -}; +class QgsPGRootItem; +class QgsPGConnectionItem; +class QgsPGSchemaItem; +class QgsPGLayerItem; class QgsPGRootItem : public QgsDataCollectionItem { @@ -73,5 +29,54 @@ class QgsPGRootItem : public QgsDataCollectionItem void newConnection(); }; +class QgsPGConnectionItem : public QgsDataCollectionItem +{ + Q_OBJECT + public: + QgsPGConnectionItem( QgsDataItem* parent, QString name, QString path ); + ~QgsPGConnectionItem(); + + QVector createChildren(); + virtual bool equal( const QgsDataItem *other ); + virtual QList actions(); + + QgsPostgresConn *connection() const { return mConn; } + + public slots: + void editConnection(); + void deleteConnection(); + + void setLayerType( QgsPostgresLayerProperty layerProperty ); + + private: + QgsPostgresConn *mConn; + QMap mSchemaMap; +}; + +class QgsPGSchemaItem : public QgsDataCollectionItem +{ + Q_OBJECT + public: + QgsPGSchemaItem( QgsDataItem* parent, QString name, QString path ); + ~QgsPGSchemaItem(); + + QVector createChildren(); + + void addLayer( QgsPostgresLayerProperty layerProperty ); +}; + +class QgsPGLayerItem : public QgsLayerItem +{ + Q_OBJECT + + public: + QgsPGLayerItem( QgsDataItem* parent, QString name, QString path, QgsLayerItem::LayerType layerType, QgsPostgresLayerProperty layerProperties ); + ~QgsPGLayerItem(); + + QString createUri(); + + private: + QgsPostgresLayerProperty mLayerProperty; +}; #endif // QGSPOSTGRESDATAITEMS_H diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index f8321b8e61b..7f0b1950319 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -15,14 +15,6 @@ * * ***************************************************************************/ - -// for htonl -#ifdef WIN32 -#include -#else -#include -#endif - #include #include #include @@ -31,31 +23,2684 @@ #include #include #include -#include "qgsvectorlayerimport.h" +#include "qgsvectorlayerimport.h" #include "qgsprovidercountcalcevent.h" #include "qgsproviderextentcalcevent.h" - #include "qgspostgresprovider.h" -#include "qgspostgresconnection.h" - +#include "qgspostgresconn.h" +#include "qgspgsourceselect.h" +#include "qgspostgresdataitems.h" #include "qgslogger.h" -#include "qgscredentials.h" -#include - const QString POSTGRES_KEY = "postgres"; const QString POSTGRES_DESCRIPTION = "PostgreSQL/PostGIS data provider"; -// Note: Because the the geometry type select SQL is also in the qgspgsourceselect // code this parameter is duplicated there. -static const int sGeomTypeSelectLimit = 100; +int QgsPostgresProvider::sProviderIds = 0; +const int QgsPostgresProvider::sFeatureQueueSize = 2000; -QMap QgsPostgresProvider::Conn::connectionsRO; -QMap QgsPostgresProvider::Conn::connectionsRW; -QMap QgsPostgresProvider::Conn::passwordCache; -int QgsPostgresProvider::providerIds = 0; +QgsPostgresProvider::QgsPostgresProvider( QString const & uri ) + : QgsVectorDataProvider( uri ) + , mFetching( false ) + , mValid( false ) + , mDetectedGeomType( QGis::WKBUnknown ) + , mRequestedGeomType( QGis::WKBUnknown ) + , mFeatureQueueSize( sFeatureQueueSize ) + , mUseEstimatedMetadata( false ) + , mSelectAtIdDisabled( false ) + , mConnectionRO( 0 ) + , mConnectionRW( 0 ) + , mFidCounter( 0 ) +{ + mProviderId = sProviderIds++; + QgsDebugMsg( "URI: " + uri ); + + mUri = QgsDataSourceURI( uri ); + + // populate members from the uri structure + mSchemaName = mUri.schema(); + mTableName = mUri.table(); + mGeometryColumn = mUri.geometryColumn(); + mSqlWhereClause = mUri.sql(); + mRequestedSrid = mUri.srid(); + mRequestedGeomType = mUri.geometryType(); + mIsGeography = false; + + if ( mSchemaName.isEmpty() && + mTableName.startsWith( "(SELECT", Qt::CaseInsensitive ) && + mTableName.endsWith( ")" ) ) + { + mIsQuery = true; + mQuery = mTableName; + mTableName = ""; + } + else + { + mIsQuery = false; + + if ( !mSchemaName.isEmpty() ) + { + mQuery += quotedIdentifier( mSchemaName ) + "."; + } + + if ( !mTableName.isEmpty() ) + { + mQuery += quotedIdentifier( mTableName ); + } + } + + mUseEstimatedMetadata = mUri.useEstimatedMetadata(); + mSelectAtIdDisabled = mUri.selectAtIdDisabled(); + + QgsDebugMsg( "Connection info is " + mUri.connectionInfo() ); + QgsDebugMsg( "Geometry column is: " + mGeometryColumn ); + QgsDebugMsg( "Schema is: " + mSchemaName ); + QgsDebugMsg( "Table name is: " + mTableName ); + QgsDebugMsg( "Query is: " + mQuery ); + QgsDebugMsg( "Where clause is: " + mSqlWhereClause ); + + // no table/query passed, the provider could be used to get tables + if ( mQuery.isEmpty() ) + { + return; + } + + mConnectionRO = QgsPostgresConn::connectDb( mUri.connectionInfo(), true ); + if ( !mConnectionRO ) + { + return; + } + + if ( !hasSufficientPermsAndCapabilities() ) // check permissions and set capabilities + { + disconnectDb(); + return; + } + + if ( !getGeometryDetails() ) // gets srid and geometry type + { + // the table is not a geometry table + QgsMessageLog::logMessage( tr( "invalid PostgreSQL layer" ), tr( "PostGIS" ) ); + disconnectDb(); + return; + } + + mLayerExtent.setMinimal(); + mFeaturesCounted = -1; + + // set the primary key + if ( !determinePrimaryKey() ) + { + mValid = false; + disconnectDb(); + return; + } + + // Set the postgresql message level so that we don't get the + // 'there is no transaction in progress' warning. +#ifndef QGISDEBUG + mConnectionRO->PQexecNR( "set client_min_messages to error" ); +#endif + + //fill type names into sets + mNativeTypes + // integer types + << QgsVectorDataProvider::NativeType( tr( "Whole number (smallint - 16bit)" ), "int2", QVariant::Int ) + << QgsVectorDataProvider::NativeType( tr( "Whole number (integer - 32bit)" ), "int4", QVariant::Int ) + << QgsVectorDataProvider::NativeType( tr( "Whole number (integer - 64bit)" ), "int8", QVariant::LongLong ) + << QgsVectorDataProvider::NativeType( tr( "Decimal number (numeric)" ), "numeric", QVariant::Double, 1, 20, 0, 20 ) + << QgsVectorDataProvider::NativeType( tr( "Decimal number (decimal)" ), "decimal", QVariant::Double, 1, 20, 0, 20 ) + + // floating point + << QgsVectorDataProvider::NativeType( tr( "Decimal number (real)" ), "real", QVariant::Double ) + << QgsVectorDataProvider::NativeType( tr( "Decimal number (double)" ), "double precision", QVariant::Double ) + + // string types + << QgsVectorDataProvider::NativeType( tr( "Text, fixed length (char)" ), "char", QVariant::String, 1, 255 ) + << QgsVectorDataProvider::NativeType( tr( "Text, limited variable length (varchar)" ), "varchar", QVariant::String, 1, 255 ) + << QgsVectorDataProvider::NativeType( tr( "Text, unlimited length (text)" ), "text", QVariant::String ) + ; + + QString key; + switch ( mPrimaryKeyType ) + { + case pktOid: + key = "oid"; + break; + case pktTid: + key = "tid"; + break; + case pktInt: + Q_ASSERT( mPrimaryKeyAttrs.size() == 1 ); + Q_ASSERT( mAttributeFields.contains( mPrimaryKeyAttrs[0] ) ); + key = mAttributeFields[ mPrimaryKeyAttrs[0] ].name(); + break; + case pktFidMap: + { + QString delim; + foreach( int idx, mPrimaryKeyAttrs ) + { + key += delim + mAttributeFields[ idx ].name(); + delim = ","; + } + } + break; + case pktUnknown: + mValid = false; + break; + } + + if ( mValid ) + { + mUri.setKeyColumn( key ); + setDataSourceUri( mUri.uri() ); + } + else + { + disconnectDb(); + } +} + +QgsPostgresProvider::~QgsPostgresProvider() +{ + disconnectDb(); + + QgsDebugMsg( "deconstructing." ); + + //pLog.flush(); +} + +void QgsPostgresProvider::disconnectDb() +{ + if ( mFetching ) + { + mConnectionRO->closeCursor( QString( "qgisf%1" ).arg( mProviderId ) ); + mFetching = false; + } + + if ( mConnectionRO ) + { + mConnectionRO->disconnect(); + } + + if ( mConnectionRW ) + { + mConnectionRW->disconnect(); + } +} + +QString QgsPostgresProvider::storageType() const +{ + return "PostgreSQL database with PostGIS extension"; +} + +bool QgsPostgresProvider::declareCursor( + const QString &cursorName, + const QgsAttributeList &fetchAttributes, + bool fetchGeometry, + QString whereClause ) +{ + if ( fetchGeometry && mGeometryColumn.isNull() ) + { + return false; + } + + try + { + QString query = "SELECT ", delim = ""; + + if ( fetchGeometry ) + { + if ( mIsGeography ) + { + query += QString( "st_asbinary(%1)" ) + .arg( quotedIdentifier( mGeometryColumn ) ); + } + else + { + query += QString( "%1(%2,'%3')" ) + .arg( mConnectionRO->majorVersion() < 2 ? "asbinary" : "st_asbinary" ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( endianString() ); + } + delim = ","; + } + + switch ( mPrimaryKeyType ) + { + case pktOid: + query += delim + "oid"; + delim = ","; + break; + + case pktTid: + query += delim + "ctid"; + delim = ","; + break; + + case pktInt: + query += delim + quotedIdentifier( field( mPrimaryKeyAttrs[0] ).name() ); + delim = ","; + break; + + case pktFidMap: + foreach( int idx, mPrimaryKeyAttrs ) + { + query += delim + mConnectionRO->fieldExpression( field( idx ) ); + delim = ","; + } + break; + + case pktUnknown: + QgsDebugMsg( "Cannot declare cursor without primary key." ); + return false; + break; + } + + foreach( int idx, fetchAttributes ) + { + if ( mPrimaryKeyAttrs.contains( idx ) ) + continue; + + query += delim + mConnectionRO->fieldExpression( field( idx ) ); + } + + query += " FROM " + mQuery; + + if ( !whereClause.isEmpty() ) + query += QString( " WHERE %1" ).arg( whereClause ); + + if ( !mConnectionRO->openCursor( cursorName, query ) ) + { + // reloading the fields might help next time around + rewind(); + return false; + } + } + catch ( PGFieldNotFound ) + { + return false; + } + + return true; +} + +static bool operator<( const QVariant &a, const QVariant &b ) +{ + if ( a.isNull() || b.isNull() ) + return false; + + if ( a.type() == b.type() ) + { + switch ( a.type() ) + { + case QVariant::Int: + case QVariant::Char: + return a.toInt() < b.toInt(); + + case QVariant::Double: + return a.toDouble() < b.toDouble(); + + case QVariant::LongLong: + return a.toLongLong() < b.toLongLong(); + + case QVariant::List: + { + QList al = a.toList(); + QList bl = b.toList(); + + int i, n = qMin( al.size(), bl.size() ); + for ( i = 0; i < n && al[i] == bl[i]; i++ ) + ; + + if ( i == n ) + return al.size() < bl.size(); + else + return al[i] < bl[i]; + } + break; + + case QVariant::StringList: + { + QStringList al = a.toStringList(); + QStringList bl = b.toStringList(); + + int i, n = qMin( al.size(), bl.size() ); + for ( i = 0; i < n && al[i] == bl[i]; i++ ) + ; + + if ( i == n ) + return al.size() < bl.size(); + else + return al[i] < bl[i]; + } + break; + + case QVariant::Date: + return a.toDate() < b.toDate(); + + case QVariant::Time: + return a.toTime() < b.toTime(); + + case QVariant::DateTime: + return a.toDateTime() < b.toDateTime(); + + case QVariant::Bool: + return a.toBool() < b.toBool(); + + case QVariant::UInt: + return a.toUInt() < b.toUInt(); + + case QVariant::ULongLong: + return a.toULongLong() < b.toULongLong(); + + default: + break; + } + } + + return a.canConvert( QVariant::String ) && b.canConvert( QVariant::String ) && a.toString() < b.toString(); +} + +QgsFeatureId QgsPostgresProvider::lookupFid( const QVariant &v ) +{ + QMap::const_iterator it = mKeyToFid.find( v ); + + if ( it != mKeyToFid.constEnd() ) + { + return it.value(); + } + + mFidToKey.insert( ++mFidCounter, v ); + mKeyToFid.insert( v, mFidCounter ); + + return mFidCounter; +} + +bool QgsPostgresProvider::getFeature( QgsPostgresResult &queryResult, int row, bool fetchGeometry, + QgsFeature &feature, + const QgsAttributeList &fetchAttributes ) +{ + try + { + feature.clearAttributeMap(); + + int col = 0; + + if ( fetchGeometry ) + { + int returnedLength = ::PQgetlength( queryResult.result(), row, col ); + if ( returnedLength > 0 ) + { + unsigned char *featureGeom = new unsigned char[returnedLength + 1]; + memset( featureGeom, 0, returnedLength + 1 ); + memcpy( featureGeom, PQgetvalue( queryResult.result(), row, col ), returnedLength ); + feature.setGeometryAndOwnership( featureGeom, returnedLength + 1 ); + } + else + { + feature.setGeometryAndOwnership( 0, 0 ); + QgsMessageLog::logMessage( tr( "Couldn't get the feature geometry in binary form" ), tr( "PostGIS" ) ); + } + + col++; + } + + QgsFeatureId fid; + + switch ( mPrimaryKeyType ) + { + case pktOid: + case pktInt: + case pktTid: + fid = mConnectionRO->getBinaryInt( queryResult, row, col++ ); + break; + + case pktFidMap: + { + QList primaryKeyVals; + + foreach( int idx, mPrimaryKeyAttrs ) + { + const QgsField &fld = field( idx ); + + QVariant v = convertValue( fld.type(), queryResult.PQgetvalue( row, col ) ); + primaryKeyVals << v; + + if ( fetchAttributes.contains( idx ) ) + feature.addAttribute( idx, v ); + + col++; + } + + fid = lookupFid( QVariant( primaryKeyVals ) ); + } + break; + + case pktUnknown: + Q_ASSERT( !"FAILURE: cannot get feature with unknown primary key" ); + return false; + } + + feature.setFeatureId( fid ); + QgsDebugMsgLevel( QString( "fid=%1" ).arg( fid ), 4 ); + + // iterate attributes + foreach( int idx, fetchAttributes ) + { + if ( mPrimaryKeyAttrs.contains( idx ) ) + continue; + + const QgsField &fld = field( idx ); + + QVariant v = convertValue( fld.type(), queryResult.PQgetvalue( row, col ) ); + feature.addAttribute( idx, v ); + + col++; + } + + return true; + } + catch ( PGFieldNotFound ) + { + return false; + } +} + +void QgsPostgresProvider::select( QgsAttributeList fetchAttributes, QgsRectangle rect, bool fetchGeometry, bool useIntersect ) +{ + QString cursorName = QString( "qgisf%1" ).arg( mProviderId ); + + if ( mFetching ) + { + mConnectionRO->closeCursor( cursorName ); + mFetching = false; + + while ( !mFeatureQueue.empty() ) + { + mFeatureQueue.dequeue(); + } + } + + QString whereClause; + + if ( !rect.isEmpty() && !mGeometryColumn.isNull() ) + { + if ( mIsGeography ) + { + rect = QgsRectangle( -180.0, -90.0, 180.0, 90.0 ).intersect( &rect ); + if ( !rect.isFinite() ) + whereClause = "false"; + } + + if ( whereClause.isEmpty() ) + { + QString qBox = QString( "%1('BOX3D(%2)'::box3d,%3)" ) + .arg( mConnectionRO->majorVersion() < 2 ? "setsrid" : "st_setsrid" ) + .arg( rect.asWktCoordinates() ) + .arg( mDetectedSrid ); + + whereClause = QString( "%1 && %2" ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( qBox ); + if ( useIntersect ) + { + whereClause += QString( " AND %1(%2,%3)" ) + .arg( mConnectionRO->majorVersion() < 2 ? "intersects" : "st_intersects" ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( qBox ); + } + } + + if ( !mRequestedSrid.isEmpty() && mRequestedSrid != mDetectedSrid ) + { + whereClause += QString( " AND %1(%2)=%3" ) + .arg( mConnectionRO->majorVersion() < 2 ? "srid" : "st_srid" ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( mRequestedSrid ); + } + + if ( mRequestedGeomType != QGis::WKBUnknown && mRequestedGeomType != mDetectedGeomType ) + { + whereClause += QString( " AND upper(geometrytype(%1))=%2" ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( quotedValue( QgsPostgresConn::postgisGeometryTypeName( mRequestedGeomType ) ) ); + } + } + + if ( !mSqlWhereClause.isEmpty() ) + { + if ( !whereClause.isEmpty() ) + whereClause += " AND "; + + whereClause += "(" + mSqlWhereClause + ")"; + } + + mFetchGeom = fetchGeometry; + mAttributesToFetch = fetchAttributes; + if ( !declareCursor( cursorName, fetchAttributes, fetchGeometry, whereClause ) ) + return; + + mFetching = true; + mFetched = 0; +} + +bool QgsPostgresProvider::nextFeature( QgsFeature& feature ) +{ + feature.setValid( false ); + if ( !mValid ) + { + QgsMessageLog::logMessage( tr( "Read attempt on an invalid postgresql data source" ), tr( "PostGIS" ) ); + return false; + } + + if ( !mFetching ) + { + QgsMessageLog::logMessage( tr( "nextFeature() without select()" ), tr( "PostGIS" ) ); + return false; + } + + QString cursorName = QString( "qgisf%1" ).arg( mProviderId ); + + if ( mFeatureQueue.empty() ) + { + QString fetch = QString( "FETCH FORWARD %1 FROM %2" ).arg( mFeatureQueueSize ).arg( cursorName ); + QgsDebugMsgLevel( QString( "fetching %1 features." ).arg( mFeatureQueueSize ), 3 ); + if ( mConnectionRO->PQsendQuery( fetch ) == 0 ) // fetch features asynchronously + { + QgsMessageLog::logMessage( tr( "Fetching from cursor %1 failed\nDatabase error: %2" ).arg( cursorName ).arg( mConnectionRO->PQerrorMessage() ), tr( "PostGIS" ) ); + } + + QgsPostgresResult queryResult; + for ( ;; ) + { + queryResult = mConnectionRO->PQgetResult(); + if ( !queryResult.result() ) + break; + + int rows = queryResult.PQntuples(); + if ( rows == 0 ) + continue; + + for ( int row = 0; row < rows; row++ ) + { + mFeatureQueue.enqueue( QgsFeature() ); + getFeature( queryResult, row, mFetchGeom, mFeatureQueue.back(), mAttributesToFetch ); + } // for each row in queue + } + } + + if ( mFeatureQueue.empty() ) + { + QgsDebugMsg( QString( "Finished after %1 features" ).arg( mFetched ) ); + mConnectionRO->closeCursor( cursorName ); + mFetching = false; + if ( mFeaturesCounted < mFetched ) + { + QgsDebugMsg( QString( "feature count adjusted from %1 to %2" ).arg( mFeaturesCounted ).arg( mFetched ) ); + mFeaturesCounted = mFetched; + } + return false; + } + + // Now return the next feature from the queue + if ( mFetchGeom ) + { + QgsGeometry* featureGeom = mFeatureQueue.front().geometryAndOwnership(); + feature.setGeometry( featureGeom ); + } + else + { + feature.setGeometryAndOwnership( 0, 0 ); + } + feature.setFeatureId( mFeatureQueue.front().id() ); + feature.setAttributeMap( mFeatureQueue.front().attributeMap() ); + + mFeatureQueue.dequeue(); + mFetched++; + + feature.setValid( true ); + return true; +} + +QString QgsPostgresProvider::pkParamWhereClause( int offset ) const +{ + QString whereClause; + + switch ( mPrimaryKeyType ) + { + case pktTid: + whereClause = QString( "ctid=$%1" ).arg( offset ); + break; + + case pktOid: + whereClause = QString( "oid=$%1" ).arg( offset ); + break; + + case pktInt: + Q_ASSERT( mPrimaryKeyAttrs.size() == 1 ); + whereClause = QString( "%1=$%2" ).arg( quotedIdentifier( field( mPrimaryKeyAttrs[0] ).name() ) ).arg( offset ); + break; + + case pktFidMap: + { + QString delim = ""; + for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ ) + { + int idx = mPrimaryKeyAttrs[i]; + const QgsField &fld = field( idx ); + + whereClause += delim + QString( "%1=$%2" ).arg( mConnectionRO->fieldExpression( fld ) ).arg( offset++ ); + delim = " AND "; + } + } + break; + + case pktUnknown: + Q_ASSERT( !"FAILURE: Primary key unknown" ); + whereClause = "NULL"; + break; + } + + if ( !mSqlWhereClause.isEmpty() ) + { + if ( !whereClause.isEmpty() ) + whereClause += " AND "; + + whereClause += "(" + mSqlWhereClause + ")"; + } + + return whereClause; +} + +void QgsPostgresProvider::appendPkParams( QgsFeatureId featureId, QStringList ¶ms ) const +{ + switch ( mPrimaryKeyType ) + { + case pktTid: + case pktOid: + case pktInt: + params << quotedValue( QString::number( featureId ) ); + break; + + case pktFidMap: + { + QList pkVals; + QMap::const_iterator it = mFidToKey.find( featureId ); + if ( it != mFidToKey.constEnd() ) + { + pkVals = it.value().toList(); + Q_ASSERT( pkVals.size() == mPrimaryKeyAttrs.size() ); + } + + for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ ) + { + int idx = mPrimaryKeyAttrs[i]; + + if ( i < pkVals.size() ) + { + params << pkVals[i].toString(); + } + else + { + QgsDebugMsg( QString( "FAILURE: Key value %1 for feature %2 not found." ).arg( idx ).arg( featureId ) ); + params << "NULL"; + } + } + + QgsDebugMsg( "keys params: " + params.join( "; " ) ); + } + break; + + case pktUnknown: + Q_ASSERT( !"FAILURE: Primary key unknown" ); + break; + } +} + +QString QgsPostgresProvider::whereClause( QgsFeatureId featureId ) const +{ + QString whereClause; + + switch ( mPrimaryKeyType ) + { + case pktTid: + whereClause = QString( "ctid='(%1,%2)'" ) + .arg( FID_TO_NUMBER( featureId ) >> 16 ) + .arg( FID_TO_NUMBER( featureId ) & 0xffff ); + break; + + case pktOid: + whereClause = QString( "oid=%1" ).arg( featureId ); + break; + + case pktInt: + Q_ASSERT( mPrimaryKeyAttrs.size() == 1 ); + whereClause = QString( "%1=%2" ).arg( quotedIdentifier( field( mPrimaryKeyAttrs[0] ).name() ) ).arg( featureId ); + break; + + case pktFidMap: + { + QMap::const_iterator it = mFidToKey.find( featureId ); + if ( it != mFidToKey.constEnd() ) + { + QList pkVals = it.value().toList(); + + Q_ASSERT( pkVals.size() == mPrimaryKeyAttrs.size() ); + + QString delim = ""; + for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ ) + { + int idx = mPrimaryKeyAttrs[i]; + const QgsField &fld = field( idx ); + + whereClause += delim + QString( "%1=%2" ).arg( mConnectionRO->fieldExpression( fld ) ).arg( quotedValue( pkVals[i].toString() ) ); + delim = " AND "; + } + } + else + { + QgsDebugMsg( QString( "FAILURE: Key values for feature %1 not found." ).arg( featureId ) ); + whereClause = "NULL"; + } + } + break; + + case pktUnknown: + Q_ASSERT( !"FAILURE: Primary key unknown" ); + whereClause = "NULL"; + break; + } + + if ( !mSqlWhereClause.isEmpty() ) + { + if ( !whereClause.isEmpty() ) + whereClause += " AND "; + + whereClause += "(" + mSqlWhereClause + ")"; + } + + return whereClause; +} + +bool QgsPostgresProvider::featureAtId( QgsFeatureId featureId, QgsFeature& feature, bool fetchGeometry, QgsAttributeList fetchAttributes ) +{ + feature.setValid( false ); + +#if 0 + if ( mFeatureMap.contains( featureId ) ) + { + QgsFeature * fpointer = &feature; + *fpointer = mFeatureMap.value( featureId ); + QgsDebugMsg( QString( "retrieve feature %1 from cache" ).arg( featureId ) ); + + mPriorityIds.removeAll( featureId ); + mPriorityIds.prepend( featureId ); + return true; + } +#endif + + QString cursorName = QString( "qgisfid%1" ).arg( mProviderId ); + + if ( !declareCursor( cursorName, fetchAttributes, fetchGeometry, whereClause( featureId ) ) ) + return false; + + QgsPostgresResult queryResult = mConnectionRO->PQexec( QString( "fetch forward 1 from %1" ).arg( cursorName ) ); + + int rows = queryResult.PQntuples(); + if ( rows == 0 ) + { + QgsMessageLog::logMessage( tr( "feature %1 not found" ).arg( featureId ), tr( "PostGIS" ) ); + mConnectionRO->closeCursor( cursorName ); + return false; + } + else if ( rows != 1 ) + { + QgsMessageLog::logMessage( tr( "found %1 features instead of just one." ).arg( rows ), tr( "PostGIS" ) ); + } + + bool gotit = getFeature( queryResult, 0, fetchGeometry, feature, fetchAttributes ); + + mConnectionRO->closeCursor( cursorName ); + + feature.setValid( gotit ); + +#if 0 + if ( gotit ) + { + mFeatureMap.insert( featureId, feature ); + mPriorityIds.prepend( featureId ); + if ( mPriorityIds.count() == 20 ) + { + mFeatureMap.remove( mPriorityIds.takeLast() ); + } + } +#endif + + return gotit; +} + +void QgsPostgresProvider::setExtent( QgsRectangle& newExtent ) +{ + mLayerExtent.setXMaximum( newExtent.xMaximum() ); + mLayerExtent.setXMinimum( newExtent.xMinimum() ); + mLayerExtent.setYMaximum( newExtent.yMaximum() ); + mLayerExtent.setYMinimum( newExtent.yMinimum() ); +} + +/** + * Return the feature type + */ +QGis::WkbType QgsPostgresProvider::geometryType() const +{ + return mRequestedGeomType != QGis::WKBUnknown ? mRequestedGeomType : mDetectedGeomType; +} + +const QgsField &QgsPostgresProvider::field( int index ) const +{ + QgsFieldMap::const_iterator it = mAttributeFields.find( index ); + + if ( it == mAttributeFields.constEnd() ) + { + QgsLogger::warning( QString( "FAILURE: Field %1 not found." ).arg( index ) ); + throw PGFieldNotFound(); + } + + return it.value(); +} + +/** + * Return the number of fields + */ +uint QgsPostgresProvider::fieldCount() const +{ + return mAttributeFields.size(); +} + +const QgsFieldMap & QgsPostgresProvider::fields() const +{ + return mAttributeFields; +} + +QString QgsPostgresProvider::dataComment() const +{ + return mDataComment; +} + +void QgsPostgresProvider::rewind() +{ + if ( mFetching ) + { + //move cursor to first record + mConnectionRO->PQexecNR( QString( "move 0 in qgisf%1" ).arg( mProviderId ) ); + } + mFeatureQueue.empty(); + loadFields(); +} + +/** @todo XXX Perhaps this should be promoted to QgsDataProvider? */ +QString QgsPostgresProvider::endianString() +{ + switch ( QgsApplication::endian() ) + { + case QgsApplication::NDR: + return QString( "NDR" ); + break; + case QgsApplication::XDR: + return QString( "XDR" ); + break; + default : + return QString( "Unknown" ); + } +} + +bool QgsPostgresProvider::loadFields() +{ + if ( !mIsQuery ) + { + QgsDebugMsg( "Loading fields for table " + mTableName ); + + // Get the relation oid for use in later queries + QString sql = QString( "SELECT regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); + QgsPostgresResult tresult = mConnectionRO->PQexec( sql ); + QString tableoid = tresult.PQgetvalue( 0, 0 ); + + // Get the table description + sql = QString( "SELECT description FROM pg_description WHERE objoid=%1 AND objsubid=0" ).arg( tableoid ); + tresult = mConnectionRO->PQexec( sql ); + if ( tresult.PQntuples() > 0 ) + mDataComment = tresult.PQgetvalue( 0, 0 ); + } + + // Populate the field vector for this layer. The field vector contains + // field name, type, length, and precision (if numeric) + QString sql = QString( "SELECT * FROM %1 LIMIT 0" ).arg( mQuery ); + + QgsPostgresResult result = mConnectionRO->PQexec( sql ); + + QSet fields; + + // The queries inside this loop could possibly be combined into one + // single query - this would make the code run faster. + mAttributeFields.clear(); + for ( int i = 0; i < result.PQnfields(); i++ ) + { + QString fieldName = result.PQfname( i ); + if ( fieldName == mGeometryColumn ) + continue; + + int fldtyp = result.PQftype( i ); + QString typOid = QString().setNum( fldtyp ); + int fieldPrec = -1; + QString fieldComment( "" ); + int tableoid = result.PQftable( i ); + + sql = QString( "SELECT typname,typtype,typelem,typlen FROM pg_type WHERE oid=%1" ).arg( typOid ); + // just oid; needs more work to support array type + // "oid = (SELECT Distinct typelem FROM pg_type WHERE " //needs DISTINCT to guard against 2 or more rows on int2 + // "typelem = " + typOid + " AND typlen = -1)"; + + QgsPostgresResult oidResult = mConnectionRO->PQexec( sql ); + QString fieldTypeName = oidResult.PQgetvalue( 0, 0 ); + QString fieldTType = oidResult.PQgetvalue( 0, 1 ); + QString fieldElem = oidResult.PQgetvalue( 0, 2 ); + int fieldSize = oidResult.PQgetvalue( 0, 3 ).toInt(); + + QString formattedFieldType; + if ( tableoid > 0 ) + { + sql = QString( "SELECT attnum,pg_catalog.format_type(atttypid,atttypmod) FROM pg_attribute WHERE attrelid=%1 AND attname=%2" ) + .arg( tableoid ).arg( quotedValue( fieldName ) ); + + QgsPostgresResult tresult = mConnectionRO->PQexec( sql ); + QString attnum = tresult.PQgetvalue( 0, 0 ); + formattedFieldType = tresult.PQgetvalue( 0, 1 ); + + sql = QString( "SELECT description FROM pg_description WHERE objoid=%1 AND objsubid=%2" ) + .arg( tableoid ).arg( attnum ); + + tresult = mConnectionRO->PQexec( sql ); + if ( tresult.PQntuples() > 0 ) + fieldComment = tresult.PQgetvalue( 0, 0 ); + } + + QVariant::Type fieldType; + + if ( fieldTType == "b" ) + { + bool isArray = fieldTypeName.startsWith( "_" ); + + if ( isArray ) + fieldTypeName = fieldTypeName.mid( 1 ); + + if ( fieldTypeName == "int8" || fieldTypeName == "serial8" ) + { + fieldType = QVariant::LongLong; + fieldSize = -1; + fieldPrec = 0; + } + else if ( fieldTypeName.startsWith( "int" ) || + fieldTypeName == "serial" ) + { + fieldType = QVariant::Int; + fieldSize = -1; + fieldPrec = 0; + } + else if ( fieldTypeName == "real" || + fieldTypeName == "double precision" || + fieldTypeName.startsWith( "float" ) ) + { + fieldType = QVariant::Double; + fieldSize = -1; + fieldPrec = -1; + } + else if ( fieldTypeName == "numeric" ) + { + fieldType = QVariant::Double; + + if ( formattedFieldType == "numeric" ) + { + fieldSize = -1; + fieldPrec = -1; + } + else + { + QRegExp re( "numeric\\((\\d+),(\\d+)\\)" ); + if ( re.exactMatch( formattedFieldType ) ) + { + fieldSize = re.cap( 1 ).toInt(); + fieldPrec = re.cap( 2 ).toInt(); + } + else if ( formattedFieldType != "numeric" ) + { + QgsMessageLog::logMessage( tr( "unexpected formatted field type '%1' for field %2" ) + .arg( formattedFieldType ) + .arg( fieldName ), + tr( "PostGIS" ) ); + fieldSize = -1; + fieldPrec = -1; + } + } + } + else if ( fieldTypeName == "text" || + fieldTypeName == "bpchar" || + fieldTypeName == "varchar" || + fieldTypeName == "bool" || + fieldTypeName == "geometry" || + fieldTypeName == "money" || + fieldTypeName == "ltree" || + fieldTypeName == "uuid" || + fieldTypeName.startsWith( "time" ) || + fieldTypeName.startsWith( "date" ) ) + { + fieldType = QVariant::String; + fieldSize = -1; + } + else if ( fieldTypeName == "char" ) + { + fieldType = QVariant::String; + + QRegExp re( "char\\((\\d+)\\)" ); + if ( re.exactMatch( formattedFieldType ) ) + { + fieldSize = re.cap( 1 ).toInt(); + } + else + { + QgsMessageLog::logMessage( tr( "unexpected formatted field type '%1' for field %2" ) + .arg( formattedFieldType ) + .arg( fieldName ) ); + fieldSize = -1; + fieldPrec = -1; + } + } + else + { + QgsLogger::warning( "Field " + fieldName + " ignored, because of unsupported type " + fieldTypeName ); + continue; + } + + if ( isArray ) + { + fieldTypeName = "_" + fieldTypeName; + fieldType = QVariant::String; + fieldSize = -1; + } + } + else if ( fieldTType == "e" ) + { + // enum + fieldType = QVariant::String; + fieldSize = -1; + } + else + { + QgsLogger::warning( "Field " + fieldName + " ignored, because of unsupported type type " + fieldTType ); + continue; + } + + if ( fields.contains( fieldName ) ) + { + QgsMessageLog::logMessage( tr( "Duplicate field %1 found\n" ).arg( fieldName ), tr( "PostGIS" ) ); + return false; + } + + fields << fieldName; + + mAttributeFields.insert( i, QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment ) ); + } + + return true; +} + +bool QgsPostgresProvider::hasSufficientPermsAndCapabilities() +{ + QgsDebugMsg( "Checking for permissions on the relation" ); + + QgsPostgresResult testAccess; + if ( !mIsQuery ) + { + // Check that we can read from the table (i.e., we have select permission). + QString sql = QString( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery ); + QgsPostgresResult testAccess = mConnectionRO->PQexec( sql ); + if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK ) + { + QgsMessageLog::logMessage( tr( "Unable to access the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" ) + .arg( mQuery ) + .arg( testAccess.PQresultErrorMessage() ) + .arg( sql ), tr( "PostGIS" ) ); + return false; + } + + bool inRecovery = false; + + if ( mConnectionRO->pgVersion() >= 90000 ) + { + testAccess = mConnectionRO->PQexec( "SELECT pg_is_in_recovery()" ); + if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK || testAccess.PQgetvalue( 0, 0 ) == "t" ) + { + QgsMessageLog::logMessage( tr( "PostgreSQL is still in recovery after a database crash\n(or you are connected to a (read-only) slave).\nWrite accesses will be denied." ), tr( "PostGIS" ) ); + inRecovery = true; + } + } + + // postgres has fast access to features at id (thanks to primary key / unique index) + // the latter flag is here just for compatibility + if ( !mSelectAtIdDisabled ) + { + mEnabledCapabilities = QgsVectorDataProvider::SelectAtId | QgsVectorDataProvider::SelectGeometryAtId; + } + + if ( !inRecovery ) + { + if ( mConnectionRO->pgVersion() >= 80400 ) + { + sql = QString( "SELECT " + "has_table_privilege(%1,'DELETE')," + "has_any_column_privilege(%1,'UPDATE')," + "%2" + "has_table_privilege(%1,'INSERT')," + "current_schema()" ) + .arg( quotedValue( mQuery ) ) + .arg( mGeometryColumn.isNull() + ? QString( "'f'," ) + : QString( "has_column_privilege(%1,%2,'UPDATE')," ) + .arg( quotedValue( mQuery ) ) + .arg( quotedValue( mGeometryColumn ) ) + ); + } + else + { + sql = QString( "SELECT " + "has_table_privilege(%1,'DELETE')," + "has_table_privilege(%1,'UPDATE')," + "has_table_privilege(%1,'UPDATE')," + "has_table_privilege(%1,'INSERT')," + "current_schema()" ) + .arg( quotedValue( mQuery ) ); + } + + testAccess = mConnectionRO->PQexec( sql ); + if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK ) + { + QgsMessageLog::logMessage( tr( "Unable to determine table access privileges for the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" ) + .arg( mQuery ) + .arg( testAccess.PQresultErrorMessage() ) + .arg( sql ), + tr( "PostGIS" ) ); + return false; + } + + + if ( testAccess.PQgetvalue( 0, 0 ) == "t" ) + { + // DELETE + mEnabledCapabilities |= QgsVectorDataProvider::DeleteFeatures; + } + + if ( testAccess.PQgetvalue( 0, 1 ) == "t" ) + { + // UPDATE + mEnabledCapabilities |= QgsVectorDataProvider::ChangeAttributeValues; + } + + if ( testAccess.PQgetvalue( 0, 2 ) == "t" ) + { + // UPDATE + mEnabledCapabilities |= QgsVectorDataProvider::ChangeGeometries; + } + + if ( testAccess.PQgetvalue( 0, 3 ) == "t" ) + { + // INSERT + mEnabledCapabilities |= QgsVectorDataProvider::AddFeatures; + } + + mCurrentSchema = testAccess.PQgetvalue( 0, 4 ); + if ( mCurrentSchema == mSchemaName ) + { + mUri.clearSchema(); + } + + if ( mSchemaName == "" ) + mSchemaName = mCurrentSchema; + + sql = QString( "SELECT 1 FROM pg_class,pg_namespace WHERE " + "pg_class.relnamespace=pg_namespace.oid AND " + "pg_get_userbyid(relowner)=current_user AND " + "relname=%1 AND nspname=%2" ) + .arg( quotedValue( mTableName ) ) + .arg( quotedValue( mSchemaName ) ); + testAccess = mConnectionRO->PQexec( sql ); + if ( testAccess.PQresultStatus() == PGRES_TUPLES_OK && testAccess.PQntuples() == 1 ) + { + mEnabledCapabilities |= QgsVectorDataProvider::AddAttributes | QgsVectorDataProvider::DeleteAttributes; + } + } + } + else + { + // Check if the sql is a select query + if ( !mQuery.startsWith( "(SELECT", Qt::CaseInsensitive ) && + !mQuery.endsWith( ")" ) ) + { + QgsMessageLog::logMessage( tr( "The custom query is not a select query." ), tr( "PostGIS" ) ); + return false; + } + + // get a new alias for the subquery + int index = 0; + QString alias; + QRegExp regex; + do + { + alias = QString( "subQuery_%1" ).arg( QString::number( index++ ) ); + QString pattern = QString( "(\\\"?)%1\\1" ).arg( QRegExp::escape( alias ) ); + regex.setPattern( pattern ); + regex.setCaseSensitivity( Qt::CaseInsensitive ); + } + while ( mQuery.contains( regex ) ); + + // convert the custom query into a subquery + mQuery = QString( "%1 as %2" ) + .arg( mQuery ) + .arg( quotedIdentifier( alias ) ); + + QString sql = QString( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery ); + + testAccess = mConnectionRO->PQexec( sql ); + if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK ) + { + QgsMessageLog::logMessage( tr( "Unable to execute the query.\nThe error message from the database was:\n%1.\nSQL: %2" ) + .arg( testAccess.PQresultErrorMessage() ) + .arg( sql ), tr( "PostGIS" ) ); + return false; + } + + if ( !mSelectAtIdDisabled ) + { + mEnabledCapabilities = QgsVectorDataProvider::SelectAtId | QgsVectorDataProvider::SelectGeometryAtId; + } + } + + return true; +} + +bool QgsPostgresProvider::determinePrimaryKey() +{ + mPrimaryKeyType = pktUnknown; + if ( !loadFields() ) + { + return false; + } + + // check to see if there is an unique index on the relation, which + // can be used as a key into the table. Primary keys are always + // unique indices, so we catch them as well. + + QString sql; + if ( !mIsQuery ) + { + sql = QString( "SELECT indexrelid FROM pg_index WHERE indrelid=%1::regclass AND (indisprimary OR indisunique) ORDER BY CASE WHEN indisprimary THEN 1 ELSE 2 END LIMIT 1" ).arg( quotedValue( mQuery ) ); + QgsDebugMsg( QString( "Retrieving first primary or unique index: %1" ).arg( sql ) ); + + QgsPostgresResult res = mConnectionRO->PQexec( sql ); + QgsDebugMsg( QString( "Got %1 rows." ).arg( res.PQntuples() ) ); + + QStringList log; + + // no primary or unique indizes found + if ( res.PQntuples() == 0 ) + { + QgsDebugMsg( "Relation has no primary key -- investigating alternatives" ); + + // Two options here. If the relation is a table, see if there is + // an oid column that can be used instead. + // If the relation is a view try to find a suitable column to use as + // the primary key. + + sql = QString( "SELECT relkind FROM pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); + res = mConnectionRO->PQexec( sql ); + QString type = res.PQgetvalue( 0, 0 ); + + if ( type == "r" ) // the relation is a table + { + QgsDebugMsg( "Relation is a table. Checking to see if it has an oid column." ); + + mPrimaryKeyAttrs.clear(); + + // If there is an oid on the table, use that instead, + sql = QString( "SELECT attname FROM pg_attribute WHERE attname='oid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) ); + + res = mConnectionRO->PQexec( sql ); + if ( res.PQntuples() == 0 ) + { + // Could warn the user here that performance will suffer if + // oid isn't indexed (and that they may want to add a + // primary key to the table) + mPrimaryKeyType = pktOid; + } + else + { + sql = QString( "SELECT attname FROM pg_attribute WHERE attname='ctid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) ); + + res = mConnectionRO->PQexec( sql ); + if ( res.PQntuples() == 1 ) + { + mPrimaryKeyType = pktTid; + } + } + + QgsMessageLog::logMessage( tr( "The table has no column suitable for use as a key. Quantum GIS requires a primary key, a PostgreSQL oid column or a ctid for tables." ), tr( "PostGIS" ) ); + } + else if ( type == "v" ) // the relation is a view + { + QString primaryKey = mUri.keyColumn(); + int idx = fieldNameIndex( mUri.keyColumn() ); + + if ( idx >= 0 ) + { + // validate last used candidate + sql = QString( "SELECT pg_type.typname FROM pg_attribute,pg_type WHERE atttypid=pg_type.oid AND attname=%1 AND attrelid=%2::regclass" ) + .arg( quotedValue( primaryKey ) ) + .arg( quotedValue( mQuery ) ); + QgsDebugMsg( "Retrieving primary key type for view: " + sql ); + res = mConnectionRO->PQexec( sql ); + QgsDebugMsg( QString( "Got %1 rows." ).arg( res.PQntuples() ) ); + + QString type; + if ( res.PQresultStatus() == PGRES_TUPLES_OK && res.PQntuples() == 1 ) + { + type = res.PQgetvalue( 0, 0 ); + } + + // if mUseEstimatedMetadata is on assume that the already keyfield is still unique + + if (( type.startsWith( "int" ) || type.startsWith( "serial" ) ) && + ( mUseEstimatedMetadata || uniqueData( mQuery, primaryKey ) ) ) + { + mPrimaryKeyType = pktInt; + mPrimaryKeyAttrs << idx; + } + } + + if ( mPrimaryKeyType != pktInt ) + { + mPrimaryKeyType = pktUnknown; + } + } + else + { + QgsMessageLog::logMessage( tr( "Unexpected relation type '%1'." ).arg( type ), tr( "PostGIS" ) ); + } + } + else + { + // have a primary key or unique index + int indrelid = res.PQgetvalue( 0, 0 ).toInt(); + sql = QString( "SELECT attname FROM pg_index,pg_attribute WHERE indexrelid=%1 AND indrelid=attrelid AND pg_attribute.attnum=any(pg_index.indkey)" ).arg( indrelid ); + + QgsDebugMsg( "Retrieving key columns: " + sql ); + res = mConnectionRO->PQexec( sql ); + QgsDebugMsg( QString( "Got %1 rows." ).arg( res.PQntuples() ) ); + + bool isInt = true; + + for ( int i = 0; i < res.PQntuples(); i++ ) + { + QString name = res.PQgetvalue( i, 0 ); + + int j; + for ( j = 0; j < mAttributeFields.size() && mAttributeFields[j].name() != name; j++ ) + ; + + if ( j == mAttributeFields.size() ) + { + QgsDebugMsg( "Skipping " + name ); + continue; + } + + if ( isInt && ( mAttributeFields[j].type() == QVariant::Int || mAttributeFields[j].type() == QVariant::LongLong ) ) + isInt = false; + + mPrimaryKeyAttrs << j; + } + + mPrimaryKeyType = ( mPrimaryKeyAttrs.size() == 1 && isInt ) ? pktInt : pktFidMap; + } + } + + mValid = mPrimaryKeyType != pktUnknown; + + return mValid; +} + +bool QgsPostgresProvider::uniqueData( QString query, QString colName ) +{ + Q_UNUSED( query ); + // Check to see if the given column contains unique data + QString sql = QString( "SELECT count(distinct %1)=count(%1) FROM %2" ) + .arg( quotedIdentifier( colName ) ) + .arg( mQuery ); + + if ( !mSqlWhereClause.isEmpty() ) + { + sql += " WHERE " + mSqlWhereClause; + } + + QgsPostgresResult unique = mConnectionRO->PQexec( sql ); + + if ( unique.PQresultStatus() != PGRES_TUPLES_OK ) + { + pushError( unique.PQresultErrorMessage() ); + return false; + } + + return unique.PQntuples() == 1 && unique.PQgetvalue( 0, 0 ).startsWith( "t" ); +} + +// Returns the minimum value of an attribute +QVariant QgsPostgresProvider::minimumValue( int index ) +{ + try + { + // get the field name + const QgsField &fld = field( index ); + QString sql = QString( "SELECT min(%1) FROM %2" ) + .arg( quotedIdentifier( fld.name() ) ) + .arg( mQuery ); + + if ( !mSqlWhereClause.isEmpty() ) + { + sql += QString( " WHERE %1" ).arg( mSqlWhereClause ); + } + + QgsPostgresResult rmin = mConnectionRO->PQexec( sql ); + return convertValue( fld.type(), rmin.PQgetvalue( 0, 0 ) ); + } + catch ( PGFieldNotFound ) + { + return QVariant( QString::null ); + } +} + +// Returns the list of unique values of an attribute +void QgsPostgresProvider::uniqueValues( int index, QList &uniqueValues, int limit ) +{ + uniqueValues.clear(); + + try + { + // get the field name + const QgsField &fld = field( index ); + QString sql = QString( "SELECT DISTINCT %1 FROM %2" ) + .arg( quotedIdentifier( fld.name() ) ) + .arg( mQuery ); + + if ( !mSqlWhereClause.isEmpty() ) + { + sql += QString( " WHERE %1" ).arg( mSqlWhereClause ); + } + + sql += QString( " ORDER BY %1" ) + .arg( quotedIdentifier( fld.name() ) ); + + if ( limit >= 0 ) + { + sql += QString( " LIMIT %1" ).arg( limit ); + } + + QgsPostgresResult res = mConnectionRO->PQexec( sql ); + if ( res.PQresultStatus() == PGRES_TUPLES_OK ) + { + for ( int i = 0; i < res.PQntuples(); i++ ) + uniqueValues.append( convertValue( fld.type(), res.PQgetvalue( i, 0 ) ) ); + } + } + catch ( PGFieldNotFound ) + { + } +} + +void QgsPostgresProvider::enumValues( int index, QStringList& enumList ) +{ + enumList.clear(); + + QString typeName; + //find out type of index + QgsFieldMap::const_iterator f_it = mAttributeFields.find( index ); + if ( f_it != mAttributeFields.constEnd() ) + { + typeName = f_it.value().typeName(); + } + else + { + return; + } + + //is type an enum? + QString typeSql = QString( "SELECT typtype FROM pg_type WHERE typname=%1" ).arg( quotedValue( typeName ) ); + QgsPostgresResult typeRes = mConnectionRO->PQexec( typeSql ); + if ( typeRes.PQresultStatus() != PGRES_TUPLES_OK || typeRes.PQntuples() < 1 ) + { + return; + } + + + QString typtype = typeRes.PQgetvalue( 0, 0 ); + if ( typtype.compare( "e", Qt::CaseInsensitive ) == 0 ) + { + //try to read enum_range of attribute + if ( !parseEnumRange( enumList, f_it->name() ) ) + { + enumList.clear(); + } + } + else + { + //is there a domain check constraint for the attribute? + if ( !parseDomainCheckConstraint( enumList, f_it->name() ) ) + { + enumList.clear(); + } + } +} + +bool QgsPostgresProvider::parseEnumRange( QStringList& enumValues, const QString& attributeName ) const +{ + enumValues.clear(); + + QString enumRangeSql = QString( "SELECT enumlabel FROM pg_catalog.pg_enum WHERE enumtypid=(SELECT atttypid::regclass FROM pg_attribute WHERE attrelid=%1::regclass AND attname=%2)" ) + .arg( quotedValue( mQuery ) ) + .arg( quotedValue( attributeName ) ); + QgsPostgresResult enumRangeRes = mConnectionRO->PQexec( enumRangeSql ); + + if ( enumRangeRes.PQresultStatus() != PGRES_TUPLES_OK ) + return false; + + for ( int i = 0; i < enumRangeRes.PQntuples(); i++ ) + { + enumValues << enumRangeRes.PQgetvalue( i, 0 ); + } + + return true; +} + +bool QgsPostgresProvider::parseDomainCheckConstraint( QStringList& enumValues, const QString& attributeName ) const +{ + enumValues.clear(); + + //is it a domain type with a check constraint? + QString domainSql = QString( "SELECT domain_name FROM information_schema.columns WHERE table_name=%1 AND column_name=%2" ).arg( quotedValue( mTableName ) ).arg( quotedValue( attributeName ) ); + QgsPostgresResult domainResult = mConnectionRO->PQexec( domainSql ); + if ( domainResult.PQresultStatus() == PGRES_TUPLES_OK && domainResult.PQntuples() > 0 ) + { + //a domain type + QString domainCheckDefinitionSql = QString( "SELECT consrc FROM pg_constraint WHERE conname=(SELECT constraint_name FROM information_schema.domain_constraints WHERE domain_name=%1)" ).arg( quotedValue( domainResult.PQgetvalue( 0, 0 ) ) ); + QgsPostgresResult domainCheckRes = mConnectionRO->PQexec( domainCheckDefinitionSql ); + if ( domainCheckRes.PQresultStatus() == PGRES_TUPLES_OK && domainCheckRes.PQntuples() > 0 ) + { + QString checkDefinition = domainCheckRes.PQgetvalue( 0, 0 ); + + //we assume that the constraint is of the following form: + //(VALUE = ANY (ARRAY['a'::text, 'b'::text, 'c'::text, 'd'::text])) + //normally, postgresql creates that if the contstraint has been specified as 'VALUE in ('a', 'b', 'c', 'd') + + int anyPos = checkDefinition.indexOf( QRegExp( "VALUE\\s*=\\s*ANY\\s*\\(\\s*ARRAY\\s*\\[" ) ); + int arrayPosition = checkDefinition.lastIndexOf( "ARRAY[" ); + int closingBracketPos = checkDefinition.indexOf( "]", arrayPosition + 6 ); + + if ( anyPos == -1 || anyPos >= arrayPosition ) + { + return false; //constraint has not the required format + } + + if ( arrayPosition != -1 ) + { + QString valueList = checkDefinition.mid( arrayPosition + 6, closingBracketPos ); + QStringList commaSeparation = valueList.split( ",", QString::SkipEmptyParts ); + QStringList::const_iterator cIt = commaSeparation.constBegin(); + for ( ; cIt != commaSeparation.constEnd(); ++cIt ) + { + //get string between '' + int beginQuotePos = cIt->indexOf( "'" ); + int endQuotePos = cIt->lastIndexOf( "'" ); + if ( beginQuotePos != -1 && ( endQuotePos - beginQuotePos ) > 1 ) + { + enumValues << cIt->mid( beginQuotePos + 1, endQuotePos - beginQuotePos - 1 ); + } + } + } + return true; + } + } + return false; +} + +// Returns the maximum value of an attribute +QVariant QgsPostgresProvider::maximumValue( int index ) +{ + try + { + // get the field name + const QgsField &fld = field( index ); + QString sql = QString( "SELECT max(%1) FROM %2" ) + .arg( quotedIdentifier( fld.name() ) ) + .arg( mQuery ); + + if ( !mSqlWhereClause.isEmpty() ) + { + sql += QString( " WHERE %1" ).arg( mSqlWhereClause ); + } + + QgsPostgresResult rmax = mConnectionRO->PQexec( sql ); + return convertValue( fld.type(), rmax.PQgetvalue( 0, 0 ) ); + } + catch ( PGFieldNotFound ) + { + return QVariant( QString::null ); + } +} + + +bool QgsPostgresProvider::isValid() +{ + return mValid; +} + +QVariant QgsPostgresProvider::defaultValue( QString fieldName, QString tableName, QString schemaName ) +{ + if ( schemaName.isNull() ) + schemaName = mSchemaName; + if ( tableName.isNull() ) + tableName = mTableName; + + // Get the default column value from the Postgres information + // schema. If there is no default we return an empty string. + + // Maintaining a cache of the results of this query would be quite + // simple and if this query is called lots, could save some time. + + QString sql = QString( "SELECT column_default FROM information_schema.columns WHERE column_default IS NOT NULL AND table_schema=%1 AND table_name=%2 AND column_name=%3 " ) + .arg( quotedValue( schemaName ) ) + .arg( quotedValue( tableName ) ) + .arg( quotedValue( fieldName ) ); + + QVariant defaultValue( QString::null ); + + QgsPostgresResult result = mConnectionRO->PQexec( sql ); + + if ( result.PQntuples() == 1 ) + defaultValue = result.PQgetvalue( 0, 0 ); + + return defaultValue; +} + +QVariant QgsPostgresProvider::defaultValue( int fieldId ) +{ + try + { + return defaultValue( field( fieldId ).name() ); + } + catch ( PGFieldNotFound ) + { + return QVariant( QString::null ); + } +} + +QString QgsPostgresProvider::paramValue( QString fieldValue, const QString &defaultValue ) const +{ + if ( fieldValue.isNull() ) + return QString::null; + + if ( fieldValue == defaultValue && !defaultValue.isNull() ) + { + QgsPostgresResult result = mConnectionRW->PQexec( QString( "SELECT %1" ).arg( defaultValue ) ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + + return result.PQgetvalue( 0, 0 ); + } + + return fieldValue; +} + +bool QgsPostgresProvider::addFeatures( QgsFeatureList &flist ) +{ + if ( flist.size() == 0 ) + return true; + + if ( mIsQuery ) + return false; + + if ( !connectRW() ) + return false; + + bool returnvalue = true; + + try + { + mConnectionRW->PQexecNR( "BEGIN" ); + + // Prepare the INSERT statement + QString insert = QString( "INSERT INTO %1(" ).arg( mQuery ); + QString values = ") VALUES ("; + QString delim = ""; + int offset = 1; + + QStringList defaultValues; + QList fieldId; + + if ( !mGeometryColumn.isNull() ) + { + insert += quotedIdentifier( mGeometryColumn ); + values += QString( "%1($%2%3,%4)" ) + .arg( mConnectionRO->majorVersion() < 2 ? "geomfromwkb" : "st_geomfromwkb" ) + .arg( offset++ ) + .arg( mConnectionRW->useWkbHex() ? "" : "::bytea" ) + .arg( mDetectedSrid ); + delim = ","; + } + + if ( mPrimaryKeyType == pktInt || mPrimaryKeyType == pktFidMap ) + { + foreach( int idx, mPrimaryKeyAttrs ) + { + insert += delim + quotedIdentifier( field( idx ).name() ); + values += delim + QString( "$%1" ).arg( offset++ ); + delim = ","; + fieldId << idx; + defaultValues << defaultValue( idx ).toString(); + } + } + + const QgsAttributeMap &attributevec = flist[0].attributeMap(); + + // look for unique attribute values to place in statement instead of passing as parameter + // e.g. for defaults + for ( QgsAttributeMap::const_iterator it = attributevec.begin(); it != attributevec.end(); it++ ) + { + if ( fieldId.contains( it.key() ) ) + continue; + + QgsFieldMap::const_iterator fit = mAttributeFields.find( it.key() ); + if ( fit == mAttributeFields.end() ) + continue; + + QString fieldname = fit->name(); + + QgsDebugMsg( "Checking field against: " + fieldname ); + + if ( fieldname.isEmpty() || fieldname == mGeometryColumn ) + continue; + + int i; + for ( i = 1; i < flist.size(); i++ ) + { + const QgsAttributeMap &attributevec = flist[i].attributeMap(); + + QgsAttributeMap::const_iterator thisit = attributevec.find( it.key() ); + if ( thisit == attributevec.end() ) + break; + + if ( *thisit != *it ) + break; + } + + insert += delim + quotedIdentifier( fieldname ); + + QString defVal = defaultValue( it.key() ).toString(); + + if ( i == flist.size() ) + { + if ( *it == defVal ) + { + if ( defVal.isNull() ) + { + values += delim + "NULL"; + } + else + { + values += delim + defVal; + } + } + else if ( fit->typeName() == "geometry" ) + { + values += QString( "%1%2(%3)" ) + .arg( delim ) + .arg( mConnectionRO->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" ) + .arg( quotedValue( it->toString() ) ); + } + else if ( fit->typeName() == "geography" ) + { + values += QString( "%1st_geographyfromewkt(%2)" ) + .arg( delim ) + .arg( quotedValue( it->toString() ) ); + } + else + { + values += delim + quotedValue( it->toString() ); + } + } + else + { + // value is not unique => add parameter + if ( fit->typeName() == "geometry" ) + { + values += QString( "%1%2($%3)" ) + .arg( delim ) + .arg( mConnectionRO->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" ) + .arg( defaultValues.size() + offset ); + } + else if ( fit->typeName() == "geography" ) + { + values += QString( "%1st_geographyfromewkt($%2)" ) + .arg( delim ) + .arg( defaultValues.size() + offset ); + } + else + { + values += QString( "%1$%2" ) + .arg( delim ) + .arg( defaultValues.size() + offset ); + } + defaultValues.append( defVal ); + fieldId.append( it.key() ); + } + + delim = ","; + } + + insert += values + ")"; + + QgsDebugMsg( QString( "prepare addfeatures: %1" ).arg( insert ) ); + QgsPostgresResult stmt = mConnectionRW->PQprepare( "addfeatures", insert, fieldId.size() + offset - 1, NULL ); + if ( stmt.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( stmt ); + + QList newIds; + + for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); features++ ) + { + const QgsAttributeMap &attributevec = features->attributeMap(); + + QStringList params; + if ( !mGeometryColumn.isNull() ) + { + appendGeomParam( features->geometry(), params ); + } + + for ( int i = 0; i < fieldId.size(); i++ ) + params << paramValue( attributevec[ fieldId[i] ].toString(), defaultValues[i] ); + + QgsPostgresResult result = mConnectionRW->PQexecPrepared( "addfeatures", params ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + } + + if ( flist.size() == newIds.size() ) + for ( int i = 0; i < flist.size(); i++ ) + flist[i].setFeatureId( newIds[i] ); + + mConnectionRW->PQexecNR( "DEALLOCATE addfeatures" ); + mConnectionRW->PQexecNR( "COMMIT" ); + + mFeaturesCounted += flist.size(); + } + catch ( PGException &e ) + { + QgsMessageLog::logMessage( tr( "Error while adding features.\nError: %1" ).arg( e.errorMessage() ), tr( "PostGIS" ) ); + mConnectionRW->PQexecNR( "ROLLBACK" ); + mConnectionRW->PQexecNR( "DEALLOCATE addfeatures" ); + returnvalue = false; + } + + rewind(); + return returnvalue; +} + +bool QgsPostgresProvider::deleteFeatures( const QgsFeatureIds & id ) +{ + bool returnvalue = true; + + if ( mIsQuery ) + return false; + + if ( !connectRW() ) + return false; + + try + { + mConnectionRW->PQexecNR( "BEGIN" ); + + for ( QgsFeatureIds::const_iterator it = id.begin(); it != id.end(); ++it ) + { + QString sql = QString( "DELETE FROM %1 WHERE %2" ) + .arg( mQuery ).arg( whereClause( *it ) ); + QgsDebugMsg( "delete sql: " + sql ); + + //send DELETE statement and do error handling + QgsPostgresResult result = mConnectionRW->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + + QVariant v = mFidToKey[ *it ]; + mFidToKey.remove( *it ); + mKeyToFid.remove( v ); + } + + mConnectionRW->PQexecNR( "COMMIT" ); + + mFeaturesCounted -= id.size(); + } + catch ( PGException &e ) + { + QgsMessageLog::logMessage( tr( "Error while deleting features.\nError: %1" ).arg( e.errorMessage() ), tr( "PostGIS" ) ); + mConnectionRW->PQexecNR( "ROLLBACK" ); + returnvalue = false; + } + rewind(); + return returnvalue; +} + +bool QgsPostgresProvider::addAttributes( const QList &attributes ) +{ + bool returnvalue = true; + + if ( mIsQuery ) + return false; + + if ( !connectRW() ) + return false; + + try + { + mConnectionRW->PQexecNR( "BEGIN" ); + + for ( QList::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter ) + { + QString type = iter->typeName(); + if ( type == "char" || type == "varchar" ) + { + if ( iter->length() > 0 ) + type = QString( "%1(%2)" ).arg( type ).arg( iter->length() ); + } + else if ( type == "numeric" || type == "decimal" ) + { + if ( iter->length() > 0 && iter->precision() > 0 ) + type = QString( "%1(%2,%3)" ).arg( type ).arg( iter->length() ).arg( iter->precision() ); + } + + QString sql = QString( "ALTER TABLE %1 ADD COLUMN %2 %3" ) + .arg( mQuery ) + .arg( quotedIdentifier( iter->name() ) ) + .arg( type ); + QgsDebugMsg( sql ); + + //send sql statement and do error handling + QgsPostgresResult result = mConnectionRW->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + + if ( !iter->comment().isEmpty() ) + { + sql = QString( "COMMENT ON COLUMN %1.%2 IS %3" ) + .arg( mQuery ) + .arg( quotedIdentifier( iter->name() ) ) + .arg( quotedValue( iter->comment() ) ); + result = mConnectionRW->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + } + } + + mConnectionRW->PQexecNR( "COMMIT" ); + } + catch ( PGException &e ) + { + QgsMessageLog::logMessage( tr( "Error while adding attributes. Error: %1" ).arg( e.errorMessage() ), tr( "PostGIS" ) ); + mConnectionRW->PQexecNR( "ROLLBACK" ); + returnvalue = false; + } + + rewind(); + return returnvalue; +} + +bool QgsPostgresProvider::deleteAttributes( const QgsAttributeIds& ids ) +{ + bool returnvalue = true; + + if ( mIsQuery ) + return false; + + if ( !connectRW() ) + return false; + + try + { + mConnectionRW->PQexecNR( "BEGIN" ); + + for ( QgsAttributeIds::const_iterator iter = ids.begin(); iter != ids.end(); ++iter ) + { + QgsFieldMap::const_iterator field_it = mAttributeFields.find( *iter ); + if ( field_it == mAttributeFields.constEnd() ) + continue; + + QString column = field_it->name(); + QString sql = QString( "ALTER TABLE %1 DROP COLUMN %2" ) + .arg( mQuery ) + .arg( quotedIdentifier( column ) ); + + //send sql statement and do error handling + QgsPostgresResult result = mConnectionRW->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + + //delete the attribute from mAttributeFields + mAttributeFields.remove( *iter ); + } + + mConnectionRW->PQexecNR( "COMMIT" ); + } + catch ( PGException &e ) + { + QgsMessageLog::logMessage( tr( "Error while deleting attributes.\nError: %1" ).arg( e.errorMessage() ), tr( "PostGIS" ) ); + mConnectionRW->PQexecNR( "ROLLBACK" ); + returnvalue = false; + } + + rewind(); + return returnvalue; +} + +bool QgsPostgresProvider::changeAttributeValues( const QgsChangedAttributesMap & attr_map ) +{ + bool returnvalue = true; + + if ( mIsQuery ) + return false; + + if ( !connectRW() ) + return false; + + try + { + mConnectionRW->PQexecNR( "BEGIN" ); + + // cycle through the features + for ( QgsChangedAttributesMap::const_iterator iter = attr_map.begin(); iter != attr_map.end(); ++iter ) + { + QgsFeatureId fid = iter.key(); + + // skip added features + if ( FID_IS_NEW( fid ) ) + continue; + + QString sql = QString( "UPDATE %1 SET " ).arg( mQuery ); + + const QgsAttributeMap& attrs = iter.value(); + bool pkChanged = false; + + // cycle through the changed attributes of the feature + QString delim; + for ( QgsAttributeMap::const_iterator siter = attrs.begin(); siter != attrs.end(); ++siter ) + { + try + { + QgsField fld = field( siter.key() ); + + pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() ); + + sql += delim + QString( "%1=" ).arg( quotedIdentifier( fld.name() ) ); + delim = ","; + + if ( fld.typeName() == "geometry" ) + { + sql += QString( "%1(%2)" ) + .arg( mConnectionRO->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" ) + .arg( quotedValue( siter->toString() ) ); + } + else if ( fld.typeName() == "geography" ) + { + sql += QString( "st_geographyfromewkt(%1)" ) + .arg( quotedValue( siter->toString() ) ); + } + else + { + sql += quotedValue( siter->toString() ); + } + } + catch ( PGFieldNotFound ) + { + // Field was missing - shouldn't happen + } + } + + sql += QString( " WHERE %1" ).arg( whereClause( fid ) ); + + QgsPostgresResult result = mConnectionRW->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + + // update feature id map if key was changed + if ( pkChanged && mPrimaryKeyType == pktFidMap ) + { + QVariant v = mFidToKey[ fid ]; + mFidToKey.remove( fid ); + mKeyToFid.remove( v ); + + QList k = v.toList(); + + for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ ) + { + int idx = mPrimaryKeyAttrs[i]; + if ( !attrs.contains( idx ) ) + continue; + + k[i] = attrs[ idx ]; + } + + mFidToKey.insert( fid, k ); + mKeyToFid.insert( k, fid ); + } + } + + mConnectionRW->PQexecNR( "COMMIT" ); + } + catch ( PGException &e ) + { + QgsMessageLog::logMessage( tr( "Error while changing attributes.\nError: %1" ).arg( e.errorMessage() ), tr( "PostGIS" ) ); + mConnectionRW->PQexecNR( "ROLLBACK" ); + returnvalue = false; + } + + rewind(); + + return returnvalue; +} + +void QgsPostgresProvider::appendGeomParam( QgsGeometry *geom, QStringList ¶ms ) const +{ + QString param; + unsigned char *buf = geom->asWkb(); + for ( uint i = 0; i < geom->wkbSize(); ++i ) + { + if ( mConnectionRW->useWkbHex() ) + param += QString( "%1" ).arg(( int ) buf[i], 2, 16, QChar( '0' ) ); + else + param += QString( "\\%1" ).arg(( int ) buf[i], 3, 8, QChar( '0' ) ); + } + params << param; +} + +bool QgsPostgresProvider::changeGeometryValues( QgsGeometryMap & geometry_map ) +{ + QgsDebugMsg( "entering." ); + + if ( mIsQuery || mGeometryColumn.isNull() ) + return false; + + if ( !connectRW() ) + return false; + + bool returnvalue = true; + + try + { + // Start the PostGIS transaction + mConnectionRW->PQexecNR( "BEGIN" ); + + QString update = QString( "UPDATE %1 SET %2=%3($1%4,%5) WHERE %6" ) + .arg( mQuery ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( mConnectionRW->majorVersion() < 2 ? "geomfromwkb" : "st_geomfromwkb" ) + .arg( mConnectionRW->useWkbHex() ? "" : "::bytea" ) + .arg( mDetectedSrid ) + .arg( pkParamWhereClause( 2 ) ); + + QgsDebugMsg( "updating: " + update ); + + QgsPostgresResult result = mConnectionRW->PQprepare( "updatefeatures", update, 2, NULL ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + + for ( QgsGeometryMap::iterator iter = geometry_map.begin(); + iter != geometry_map.end(); + ++iter ) + { + QgsDebugMsg( "iterating over the map of changed geometries..." ); + + if ( !iter->asWkb() ) + { + QgsDebugMsg( "empty geometry" ); + continue; + } + + QgsDebugMsg( "iterating over feature id " + FID_TO_STRING( iter.key() ) ); + + QStringList params; + appendGeomParam( &*iter, params ); + appendPkParams( iter.key(), params ); + + result = mConnectionRW->PQexecPrepared( "updatefeatures", params ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) + throw PGException( result ); + } // for each feature + + mConnectionRW->PQexecNR( "DEALLOCATE updatefeatures" ); + mConnectionRW->PQexecNR( "COMMIT" ); + } + catch ( PGException &e ) + { + QgsMessageLog::logMessage( tr( "Error while changing geometry values.\nError: %1" ).arg( e.errorMessage() ), tr( "PostGIS" ) ); + mConnectionRW->PQexecNR( "ROLLBACK" ); + mConnectionRW->PQexecNR( "DEALLOCATE updatefeatures" ); + returnvalue = false; + } + + rewind(); + + QgsDebugMsg( "exiting." ); + + return returnvalue; +} + +QgsAttributeList QgsPostgresProvider::attributeIndexes() +{ + return mAttributeFields.keys(); +} + +int QgsPostgresProvider::capabilities() const +{ + return mEnabledCapabilities; +} + +bool QgsPostgresProvider::setSubsetString( QString theSQL, bool updateFeatureCount ) +{ + QString prevWhere = mSqlWhereClause; + + mSqlWhereClause = theSQL.trimmed(); + + QString sql = QString( "SELECT * FROM %1" ).arg( mQuery ); + + if ( !mSqlWhereClause.isEmpty() ) + { + sql += QString( " WHERE %1" ).arg( mSqlWhereClause ); + } + + sql += " LIMIT 0"; + + QgsPostgresResult res = mConnectionRO->PQexec( sql ); + if ( res.PQresultStatus() != PGRES_COMMAND_OK && res.PQresultStatus() != PGRES_TUPLES_OK ) + { + pushError( res.PQresultErrorMessage() ); + mSqlWhereClause = prevWhere; + return false; + } + +#if 0 + // FIXME + if ( mPrimaryKeyType == pktInt && !uniqueData( mQuery, primaryKeyAttr ) ) + { + sqlWhereClause = prevWhere; + return false; + } +#endif + + // Update datasource uri too + mUri.setSql( theSQL ); + // Update yet another copy of the uri. Why are there 3 copies of the + // uri? Perhaps this needs some rationalisation..... + setDataSourceUri( mUri.uri() ); + + if ( updateFeatureCount ) + { + mFeaturesCounted = -1; + } + mLayerExtent.setMinimal(); + + return true; +} + +/** + * Return the feature count + */ +long QgsPostgresProvider::featureCount() const +{ + if ( mFeaturesCounted >= 0 ) + return mFeaturesCounted; + + // get total number of features + QString sql; + + // use estimated metadata even when there is a where clause, + // although we get an incorrect feature count for the subset + // - but make huge dataset usable. + if ( !mIsQuery && mUseEstimatedMetadata ) + { + sql = QString( "SELECT reltuples::int FROM pg_catalog.pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); + } + else + { + sql = QString( "SELECT count(*) FROM %1" ).arg( mQuery ); + + if ( !mSqlWhereClause.isEmpty() ) + { + sql += " WHERE " + mSqlWhereClause; + } + } + + QgsPostgresResult result = mConnectionRO->PQexec( sql ); + + QgsDebugMsg( "number of features as text: " + result.PQgetvalue( 0, 0 ) ); + + mFeaturesCounted = result.PQgetvalue( 0, 0 ).toLong(); + + QgsDebugMsg( "number of features: " + QString::number( mFeaturesCounted ) ); + + return mFeaturesCounted; +} + +QgsRectangle QgsPostgresProvider::extent() +{ + if ( mGeometryColumn.isNull() ) + return QgsRectangle(); + + if ( mIsGeography ) + return QgsRectangle( -180.0, -90.0, 180.0, 90.0 ); + + if ( mLayerExtent.isEmpty() ) + { + QString sql; + QgsPostgresResult result; + QString ext; + + // get the extents + if ( !mIsQuery && ( mUseEstimatedMetadata || mSqlWhereClause.isEmpty() ) ) + { + // do stats exists? + sql = QString( "SELECT count(*) FROM pg_stats WHERE schemaname=%1 AND tablename=%2 AND attname=%3" ) + .arg( quotedValue( mSchemaName ) ) + .arg( quotedValue( mTableName ) ) + .arg( quotedValue( mGeometryColumn ) ); + result = mConnectionRO->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_TUPLES_OK && result.PQntuples() == 1 ) + { + if ( result.PQgetvalue( 0, 0 ).toInt() > 0 ) + { + sql = QString( "SELECT reltuples::int FROM pg_catalog.pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); + result = mConnectionRO->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_TUPLES_OK + && result.PQntuples() == 1 + && result.PQgetvalue( 0, 0 ).toLong() > 0 ) + { + sql = QString( "SELECT %1(%2,%3,%4)" ) + .arg( mConnectionRO->majorVersion() < 2 ? "estimated_extent" : "st_estimated_extent" ) + .arg( quotedValue( mSchemaName ) ) + .arg( quotedValue( mTableName ) ) + .arg( quotedValue( mGeometryColumn ) ); + result = mConnectionRO->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_TUPLES_OK && result.PQntuples() == 1 && !result.PQgetisnull( 0, 0 ) ) + { + ext = result.PQgetvalue( 0, 0 ); + + // fix for what might be a postgis bug: when the extent crosses the + // dateline extent() returns -180 to 180 (which appears right), but + // estimated_extent() returns eastern bound of data (>-180) and + // 180 degrees. + if ( !ext.startsWith( "-180 " ) && ext.contains( ",180 " ) ) + { + ext.clear(); + } + } + } + else + { + // no features => ignore estimated extent + ext.clear(); + } + } + } + else + { + QgsDebugMsg( QString( "no column statistics for %1.%2.%3" ).arg( mSchemaName ).arg( mTableName ).arg( mGeometryColumn ) ); + } + } + + if ( ext.isEmpty() ) + { + sql = QString( "SELECT %1(%2) FROM %3" ) + .arg( mConnectionRO->majorVersion() < 2 ? "extent" : "st_extent" ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( mQuery ); + + if ( !mSqlWhereClause.isEmpty() ) + sql += QString( " WHERE %1" ).arg( mSqlWhereClause ); + + result = mConnectionRO->PQexec( sql ); + if ( result.PQresultStatus() != PGRES_TUPLES_OK ) + mConnectionRO->PQexecNR( "ROLLBACK" ); + else if ( result.PQntuples() == 1 && !result.PQgetisnull( 0, 0 ) ) + ext = result.PQgetvalue( 0, 0 ); + } + + if ( !ext.isEmpty() ) + { + QgsDebugMsg( "Got extents using: " + sql ); + + QRegExp rx( "\\((.+) (.+),(.+) (.+)\\)" ); + if ( ext.contains( rx ) ) + { + QStringList ex = rx.capturedTexts(); + + mLayerExtent.setXMinimum( ex[1].toDouble() ); + mLayerExtent.setYMinimum( ex[2].toDouble() ); + mLayerExtent.setXMaximum( ex[3].toDouble() ); + mLayerExtent.setYMaximum( ex[4].toDouble() ); + } + else + { + QgsMessageLog::logMessage( tr( "result of extents query invalid: %1" ).arg( ext ), tr( "PostGIS" ) ); + } + } + + QgsDebugMsg( "Set extents to: " + mLayerExtent.toString() ); + } + + return mLayerExtent; +} + +bool QgsPostgresProvider::getGeometryDetails() +{ + if ( mGeometryColumn.isNull() ) + { + mDetectedGeomType = QGis::WKBNoGeometry; + mValid = true; + return true; + } + + QgsPostgresResult result; + QString sql; + + QString schemaName = mSchemaName; + QString tableName = mTableName; + QString geomCol = mGeometryColumn; + + QString type; + QString srid; + + if ( mIsQuery ) + { + sql = QString( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ) ).arg( mQuery ); + + QgsDebugMsg( "Getting geometry column: " + sql ); + + QgsPostgresResult result = mConnectionRO->PQexec( sql ); + if ( PGRES_TUPLES_OK == result.PQresultStatus() ) + { + Oid tableoid = result.PQftable( 0 ); + int column = result.PQftablecol( 0 ); + + result = mConnectionRO->PQexec( sql ); + if ( tableoid > 0 && PGRES_TUPLES_OK == result.PQresultStatus() ) + { + sql = QString( "SELECT pg_namespace.nspname,pg_class.relname FROM pg_class,pg_namespace WHERE pg_class.relnamespace=pg_namespace.oid AND pg_class.oid=%1" ).arg( tableoid ); + result = mConnectionRO->PQexec( sql ); + + if ( PGRES_TUPLES_OK == result.PQresultStatus() && 1 == result.PQntuples() ) + { + schemaName = result.PQgetvalue( 0, 0 ); + tableName = result.PQgetvalue( 0, 1 ); + + sql = QString( "SELECT attname FROM pg_attribute WHERE attrelid=%1 AND attnum=%2" ).arg( tableoid ).arg( column ); + result = mConnectionRO->PQexec( sql ); + if ( PGRES_TUPLES_OK == result.PQresultStatus() && 1 == result.PQntuples() ) + { + geomCol = result.PQgetvalue( 0, 0 ); + } + else + { + schemaName = mSchemaName; + tableName = mTableName; + } + } + } + } + } + + // check geometry columns + sql = QString( "SELECT upper(type),srid FROM geometry_columns WHERE f_table_name=%1 AND f_geometry_column=%2 AND f_table_schema=%3" ) + .arg( quotedValue( tableName ) ) + .arg( quotedValue( geomCol ) ) + .arg( quotedValue( schemaName ) ); + + QgsDebugMsg( "Getting geometry column: " + sql ); + result = mConnectionRO->PQexec( sql ); + QgsDebugMsg( QString( "Geometry column query returned %1 rows" ).arg( result.PQntuples() ) ); + + if ( result.PQntuples() == 1 ) + { + type = result.PQgetvalue( 0, 0 ); + srid = result.PQgetvalue( 0, 1 ); + } + + if ( srid.isEmpty() || type.isEmpty() ) + { + // check geometry columns + sql = QString( "SELECT upper(type),srid FROM geography_columns WHERE f_table_name=%1 AND f_geography_column=%2 AND f_table_schema=%3" ) + .arg( quotedValue( tableName ) ) + .arg( quotedValue( geomCol ) ) + .arg( quotedValue( schemaName ) ); + + QgsDebugMsg( "Getting geography column: " + sql ); + result = mConnectionRO->PQexec( sql ); + QgsDebugMsg( "Geography column query returned " + QString::number( result.PQntuples() ) ); + + if ( result.PQntuples() == 1 ) + { + type = result.PQgetvalue( 0, 0 ); + srid = result.PQgetvalue( 0, 1 ); + + mIsGeography = true; + } + } + + if ( srid.isEmpty() || type.isEmpty() || type == "GEOMETRY" ) + { + QgsPostgresLayerProperty layerProperty; + layerProperty.schemaName = mSchemaName; + layerProperty.tableName = mTableName; + layerProperty.geometryColName = mGeometryColumn; + + QString delim = ""; + + if ( !mSqlWhereClause.isEmpty() ) + { + layerProperty.sql += delim + "(" + mSqlWhereClause + ")"; + delim = " AND "; + } + + if ( mRequestedGeomType != QGis::WKBUnknown ) + { + layerProperty.sql += QString( "%1upper(geometrytype(%2))=%3" ) + .arg( delim ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( quotedValue( QgsPostgresConn::postgisGeometryTypeName( mRequestedGeomType ) ) ); + delim = " AND "; + } + + if ( !mRequestedSrid.isEmpty() ) + { + layerProperty.sql += QString( "%1%2(%3)=%4" ) + .arg( delim ) + .arg( mConnectionRO->majorVersion() < 2 ? "srid" : "st_srid" ) + .arg( quotedIdentifier( mGeometryColumn ) ) + .arg( mRequestedSrid ); + delim = " AND "; + } + + mConnectionRO->retrieveLayerTypes( layerProperty, mUseEstimatedMetadata ); + + if ( srid.isEmpty() ) + { + srid = layerProperty.srid; + } + + if ( type.isEmpty() && !type.contains( "," ) ) + { + type = layerProperty.type; + } + } + + if ( !type.isEmpty() && !srid.isEmpty() ) + { + mDetectedGeomType = QgsPostgresConn::wkbTypeFromPostgis( type ); + mDetectedSrid = srid; + + mValid = mDetectedGeomType != QGis::WKBUnknown || mRequestedGeomType != QGis::WKBUnknown; + + if ( !mValid ) + { + QgsMessageLog::logMessage( tr( "Column %1 in %2 has a geometry type of %3, which Quantum GIS does not currently support." ) + .arg( mGeometryColumn ).arg( mQuery ).arg( type ), tr( "PostGIS" ) ); + } + } + else + { + QgsMessageLog::logMessage( tr( "Unable to get feature type or srid for %1 of %2" ).arg( mGeometryColumn ).arg( mQuery ) ); + } + + if ( mValid ) + { + // store whether the geometry includes measure value + if ( type == "POINTM" || type == "MULTIPOINTM" || + type == "LINESTRINGM" || type == "MULTILINESTRINGM" || + type == "POLYGONM" || type == "MULTIPOLYGONM" ) + { + // explicitly disable adding new features and editing of geometries + // as this would lead to corruption of measures + QgsMessageLog::logMessage( tr( "Editing and adding disabled for 2D+ layer (%1; %2)" ).arg( mGeometryColumn ).arg( mQuery ) ); + mEnabledCapabilities &= ~( QgsVectorDataProvider::ChangeGeometries | QgsVectorDataProvider::AddFeatures ); + } + + + QgsDebugMsg( "Detected SRID is " + mDetectedSrid ); + QgsDebugMsg( "Requested SRID is " + mRequestedSrid ); + QgsDebugMsg( "Detected type is " + QString::number( mDetectedGeomType ) ); + QgsDebugMsg( "Requested type is " + QString::number( mRequestedGeomType ) ); + QgsDebugMsg( "Feature type name is " + QString( QGis::qgisFeatureTypes[mDetectedGeomType] ) ); + QgsDebugMsg( "Geometry is geography " + mIsGeography ); + } + else + { + QgsMessageLog::logMessage( tr( "Failed to get geometry details for PostGIS column %1.%2." ).arg( tableName ).arg( mGeometryColumn ), tr( "PostGIS" ) ); + } + + return mValid; +} bool QgsPostgresProvider::convertField( QgsField &field ) { @@ -130,9 +2775,9 @@ QgsVectorLayerImport::ImportError QgsPostgresProvider::createEmptyLayer( QString schemaTableName = ""; if ( !schemaName.isEmpty() ) { - schemaTableName += QgsPostgresProvider::quotedIdentifier( schemaName ) + "."; + schemaTableName += quotedIdentifier( schemaName ) + "."; } - schemaTableName += QgsPostgresProvider::quotedIdentifier( tableName ); + schemaTableName += quotedIdentifier( tableName ); QgsDebugMsg( "Connection info is " + dsUri.connectionInfo() ); QgsDebugMsg( "Geometry column is: " + geometryColumn ); @@ -140,10 +2785,9 @@ QgsVectorLayerImport::ImportError QgsPostgresProvider::createEmptyLayer( QgsDebugMsg( "Table name is: " + tableName ); // create the table - Conn *conn = Conn::connectDb( dsUri.connectionInfo(), false ); - if ( conn == NULL ) + QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo(), false ); + if ( !conn ) { - QgsMessageLog::logMessage( tr( "Connection to database failed. Import of layer aborted." ), tr( "PostgreSQL" ) ); if ( errorMessage ) *errorMessage = QObject::tr( "Connection to database failed" ); return QgsVectorLayerImport::ErrConnectionFailed; @@ -200,117 +2844,63 @@ QgsVectorLayerImport::ImportError QgsPostgresProvider::createEmptyLayer( { conn->PQexecNR( "BEGIN" ); - bool exists = false; - QString sql = QString( "SELECT 1 " - "FROM pg_class AS cls JOIN pg_namespace AS nsp" - " ON nsp.oid = cls.relnamespace " - " WHERE cls.relname = %1 AND nsp.nspname = %2" ) + QString sql = QString( "SELECT 1" + " FROM pg_class AS cls JOIN pg_namespace AS nsp" + " ON nsp.oid=cls.relnamespace " + " WHERE cls.relname=%1 AND nsp.nspname=%2" ) .arg( quotedValue( tableName ) ) .arg( quotedValue( schemaName ) ); - PGresult *result = conn->PQexec( sql ); - if ( PQresultStatus( result ) == PGRES_FATAL_ERROR ) + QgsPostgresResult result = conn->PQexec( sql ); + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) throw PGException( result ); - if ( PQntuples( result ) > 0 ) - exists = true; - PQclear( result ); + + bool exists = result.PQntuples() > 0; if ( exists && overwrite ) { // delete the table if exists, then re-create it - QString sql = QString( "SELECT dropgeometrytable(%1, %2) " - "FROM pg_class AS cls JOIN pg_namespace AS nsp" - " ON nsp.oid = cls.relnamespace " - " WHERE cls.relname = %3 AND nsp.nspname = %4" ) + QString sql = QString( "SELECT DropGeometryTable(%1,%2)" + " FROM pg_class AS cls JOIN pg_namespace AS nsp" + " ON nsp.oid=cls.relnamespace " + " WHERE cls.relname=%2 AND nsp.nspname=%1" ) .arg( quotedValue( schemaName ) ) - .arg( quotedValue( tableName ) ) - .arg( quotedValue( tableName ) ) - .arg( quotedValue( schemaName ) ); + .arg( quotedValue( tableName ) ); result = conn->PQexec( sql ); - if ( PQresultStatus( result ) == PGRES_FATAL_ERROR ) + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) throw PGException( result ); - PQclear( result ); } - sql = QString( "CREATE TABLE %1 (%2 %3 PRIMARY KEY)" ) + sql = QString( "CREATE TABLE %1(%2 %3 PRIMARY KEY)" ) .arg( schemaTableName ) .arg( quotedIdentifier( primaryKey ) ) .arg( primaryKeyType ); result = conn->PQexec( sql ); - if ( PQresultStatus( result ) == PGRES_FATAL_ERROR ) + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) throw PGException( result ); - PQclear( result ); - // get geometry type, dim and srid int dim = 2; long srid = srs->postgisSrid(); - switch ( wkbType ) - { - case QGis::WKBPoint25D: - dim = 3; - case QGis::WKBPoint: - geometryType = "POINT"; - break; - - case QGis::WKBLineString25D: - dim = 3; - case QGis::WKBLineString: - geometryType = "LINESTRING"; - break; - - case QGis::WKBPolygon25D: - dim = 3; - case QGis::WKBPolygon: - geometryType = "POLYGON"; - break; - - case QGis::WKBMultiPoint25D: - dim = 3; - case QGis::WKBMultiPoint: - geometryType = "MULTIPOINT"; - break; - - case QGis::WKBMultiLineString25D: - dim = 3; - case QGis::WKBMultiLineString: - geometryType = "MULTILINESTRING"; - break; - - case QGis::WKBMultiPolygon25D: - dim = 3; - case QGis::WKBMultiPolygon: - geometryType = "MULTIPOLYGON"; - break; - - case QGis::WKBUnknown: - geometryType = "GEOMETRY"; - break; - - case QGis::WKBNoGeometry: - default: - dim = 0; - break; - } + QgsPostgresConn::postgisGeometryType( wkbType, geometryType, dim ); // create geometry column if ( !geometryType.isEmpty() ) { - sql = QString( "SELECT addgeometrycolumn(%1, %2, %3, %4, %5, %6)" ) + sql = QString( "SELECT AddGeometryColumn(%1,%2,%3,%4,%5,%6)" ) .arg( quotedValue( schemaName ) ) - .arg( QgsPostgresProvider::quotedValue( tableName ) ) - .arg( QgsPostgresProvider::quotedValue( geometryColumn ) ) + .arg( quotedValue( tableName ) ) + .arg( quotedValue( geometryColumn ) ) .arg( srid ) - .arg( QgsPostgresProvider::quotedValue( geometryType ) ) + .arg( quotedValue( geometryType ) ) .arg( dim ); result = conn->PQexec( sql ); - if ( PQresultStatus( result ) == PGRES_FATAL_ERROR ) + if ( result.PQresultStatus() == PGRES_FATAL_ERROR ) throw PGException( result ); - PQclear( result ); } else { @@ -321,17 +2911,16 @@ QgsVectorLayerImport::ImportError QgsPostgresProvider::createEmptyLayer( } catch ( PGException &e ) { - QgsMessageLog::logMessage( tr( "creation of data source %1 failed.\nError: %2" ).arg( schemaTableName ).arg( e.errorMessage() ), tr( "PostgreSQL" ) ); if ( errorMessage ) *errorMessage = QObject::tr( "Creation of data source %1 failed: \n%2" ) .arg( schemaTableName ) .arg( e.errorMessage() ); conn->PQexecNR( "ROLLBACK" ); - Conn::disconnectRW( conn ); + conn->disconnect(); return QgsVectorLayerImport::ErrCreateLayer; } - Conn::disconnectRW( conn ); + conn->disconnect(); QgsDebugMsg( "layer " + schemaTableName + " created." ); @@ -340,7 +2929,6 @@ QgsVectorLayerImport::ImportError QgsPostgresProvider::createEmptyLayer( QgsPostgresProvider *provider = new QgsPostgresProvider( dsUri.uri() ); if ( !provider->isValid() ) { - QgsMessageLog::logMessage( tr( "The layer %1 just created is not valid or not supported by the provider." ).arg( schemaTableName ), tr( "PostgreSQL" ) ); if ( errorMessage ) *errorMessage = QObject::tr( "Loading of the layer %1 failed" ).arg( schemaTableName ); @@ -377,7 +2965,6 @@ QgsVectorLayerImport::ImportError QgsPostgresProvider::createEmptyLayer( if ( !convertField( fld ) ) { - QgsMessageLog::logMessage( tr( "error creating field %1: unsupported type" ).arg( fld.name() ), tr( "PostgreSQL" ) ); if ( errorMessage ) *errorMessage = QObject::tr( "Unsupported type for field %1" ).arg( fld.name() ); @@ -400,7 +2987,6 @@ QgsVectorLayerImport::ImportError QgsPostgresProvider::createEmptyLayer( if ( !provider->addAttributes( flist ) ) { - QgsMessageLog::logMessage( tr( "error creating fields" ), tr( "PostgreSQL" ) ); if ( errorMessage ) *errorMessage = QObject::tr( "Creation of fields failed" ); @@ -413,3971 +2999,16 @@ QgsVectorLayerImport::ImportError QgsPostgresProvider::createEmptyLayer( return QgsVectorLayerImport::NoError; } - - -QgsPostgresProvider::QgsPostgresProvider( QString const & uri ) - : QgsVectorDataProvider( uri ) - , mFetching( false ) - , mIsDbPrimaryKey( false ) - , geomType( QGis::WKBUnknown ) - , mFeatureQueueSize( 200 ) - , mUseEstimatedMetadata( false ) - , mSelectAtIdDisabled( false ) - , mPrimaryKeyDefault( QString::null ) -{ - // assume this is a valid layer until we determine otherwise - valid = true; - - providerId = providerIds++; - - QgsDebugMsg( "URI: " + uri ); - - mUri = QgsDataSourceURI( uri ); - - // populate members from the uri structure - mSchemaName = mUri.schema(); - mTableName = mUri.table(); - geometryColumn = mUri.geometryColumn(); - sqlWhereClause = mUri.sql(); - isGeography = false; - - if ( mSchemaName.isEmpty() && - mTableName.startsWith( "(select", Qt::CaseInsensitive ) && - mTableName.endsWith( ")" ) ) - { - isQuery = true; - mQuery = mTableName; - mTableName = ""; - } - else - { - isQuery = false; - if ( !mSchemaName.isEmpty() ) - { - mQuery += quotedIdentifier( mSchemaName ) + "."; - } - if ( !mTableName.isEmpty() ) - { - mQuery += quotedIdentifier( mTableName ); - } - } - - primaryKey = mUri.keyColumn(); - mUseEstimatedMetadata = mUri.useEstimatedMetadata(); - mSelectAtIdDisabled = mUri.selectAtIdDisabled(); - - QgsDebugMsg( "Connection info is " + mUri.connectionInfo() ); - QgsDebugMsg( "Geometry column is: " + geometryColumn ); - QgsDebugMsg( "Schema is: " + mSchemaName ); - QgsDebugMsg( "Table name is: " + mTableName ); - QgsDebugMsg( "Query is: " + mQuery ); - QgsDebugMsg( "Where clause is: " + sqlWhereClause ); - - connectionRW = NULL; - connectionRO = NULL; - - // no table/query passed, the provider could be used to get tables - if ( mQuery.isEmpty() ) - { - return; - } - - connectionRO = Conn::connectDb( mUri.connectionInfo(), true ); - if ( connectionRO == NULL ) - { - valid = false; - return; - } - - if ( !hasSufficientPermsAndCapabilities() ) // check permissions and set capabilities - { - valid = false; - disconnectDb(); - return; - } - - if ( !getGeometryDetails() ) // gets srid and geometry type - { - // the table is not a geometry table - featuresCounted = 0; - valid = false; - - QgsMessageLog::logMessage( tr( "invalid PostgreSQL layer" ), tr( "PostgreSQL" ) ); - disconnectDb(); - return; - } - - deduceEndian(); - layerExtent.setMinimal(); - featuresCounted = -1; - - // set the primary key - getPrimaryKey(); - - // load the field list - if ( !loadFields() ) - { - valid = false; - disconnectDb(); - return; - } - - // Set the postgresql message level so that we don't get the - // 'there is no transaction in progress' warning. -#ifndef QGISDEBUG - connectionRO->PQexecNR( "set client_min_messages to error" ); -#endif - - // Kick off the long running threads - - //fill type names into sets - mNativeTypes - // integer types - << QgsVectorDataProvider::NativeType( tr( "Whole number (smallint - 16bit)" ), "int2", QVariant::Int ) - << QgsVectorDataProvider::NativeType( tr( "Whole number (integer - 32bit)" ), "int4", QVariant::Int ) - << QgsVectorDataProvider::NativeType( tr( "Whole number (integer - 64bit)" ), "int8", QVariant::LongLong ) - << QgsVectorDataProvider::NativeType( tr( "Decimal number (numeric)" ), "numeric", QVariant::Double, 1, 20, 0, 20 ) - << QgsVectorDataProvider::NativeType( tr( "Decimal number (decimal)" ), "decimal", QVariant::Double, 1, 20, 0, 20 ) - - // floating point - << QgsVectorDataProvider::NativeType( tr( "Decimal number (real)" ), "real", QVariant::Double ) - << QgsVectorDataProvider::NativeType( tr( "Decimal number (double)" ), "double precision", QVariant::Double ) - - // string types - << QgsVectorDataProvider::NativeType( tr( "Text, fixed length (char)" ), "char", QVariant::String, 1, 255 ) - << QgsVectorDataProvider::NativeType( tr( "Text, limited variable length (varchar)" ), "varchar", QVariant::String, 1, 255 ) - << QgsVectorDataProvider::NativeType( tr( "Text, unlimited length (text)" ), "text", QVariant::String ) - ; - - if ( primaryKey.isEmpty() ) - { - valid = false; - } - else - { - mUri.setKeyColumn( primaryKey ); - setDataSourceUri( mUri.uri() ); - } - - // Close the database connection if the layer isn't going to be loaded. - if ( !valid ) - disconnectDb(); -} - -QgsPostgresProvider::~QgsPostgresProvider() -{ - disconnectDb(); - - QgsDebugMsg( "deconstructing." ); - - //pLog.flush(); -} - -QgsPostgresProvider::Conn *QgsPostgresProvider::Conn::connectDb( const QString &conninfo, bool readonly ) -{ - QMap &connections = - readonly ? QgsPostgresProvider::Conn::connectionsRO : QgsPostgresProvider::Conn::connectionsRW; - - if ( connections.contains( conninfo ) ) - { - QgsDebugMsg( QString( "Using cached connection for %1" ).arg( conninfo ) ); - connections[conninfo]->ref++; - return connections[conninfo]; - } - - QgsDebugMsg( QString( "New postgres connection for " ) + conninfo ); - - PGconn *pd = PQconnectdb( conninfo.toLocal8Bit() ); // use what is set based on locale; after connecting, use Utf8 - // check the connection status - if ( PQstatus( pd ) != CONNECTION_OK ) - { - QgsDataSourceURI uri( conninfo ); - QString username = uri.username(); - QString password = uri.password(); - - while ( PQstatus( pd ) != CONNECTION_OK ) - { - bool ok = QgsCredentials::instance()->get( conninfo, username, password, QString::fromUtf8( PQerrorMessage( pd ) ) ); - if ( !ok ) - break; - - ::PQfinish( pd ); - - if ( !username.isEmpty() ) - uri.setUsername( username ); - - if ( !password.isEmpty() ) - uri.setPassword( password ); - - QgsDebugMsg( "Connecting to " + uri.connectionInfo() ); - pd = PQconnectdb( uri.connectionInfo().toLocal8Bit() ); - } - - if ( PQstatus( pd ) == CONNECTION_OK ) - QgsCredentials::instance()->put( conninfo, username, password ); - } - - if ( PQstatus( pd ) != CONNECTION_OK ) - { - ::PQfinish( pd ); - QgsMessageLog::logMessage( tr( "Connection to database failed" ), tr( "PostgreSQL" ) ); - return NULL; - } - - //set client encoding to unicode because QString uses UTF-8 anyway - QgsDebugMsg( "setting client encoding to UNICODE" ); - - int errcode = PQsetClientEncoding( pd, QString( "UNICODE" ).toLocal8Bit() ); - - if ( errcode == 0 ) - { - QgsDebugMsg( "encoding successfully set" ); - } - else if ( errcode == -1 ) - { - QgsMessageLog::logMessage( tr( "error in setting encoding" ), tr( "PostgreSQL" ) ); - } - else - { - QgsMessageLog::logMessage( tr( "undefined return value from encoding setting" ), tr( "PostgreSQL" ) ); - } - - QgsDebugMsg( "Connection to the database was successful" ); - - Conn *conn = new Conn( pd ); - - /* Check to see if we have working PostGIS support */ - if ( conn->postgisVersion().isNull() ) - { - showMessageBox( tr( "No PostGIS Support!" ), - tr( "Your database has no working PostGIS support.\n" ) ); - conn->PQfinish(); - delete conn; - return NULL; - } - - connections.insert( conninfo, conn ); - - if ( !conn->PQexecNR( "SET application_name='Quantum GIS'" ) ) - { - conn->PQexecNR( "ROLLBACK" ); - } - - /* Check to see if we have GEOS support and if not, warn the user about - the problems they will see :) */ - QgsDebugMsg( "Checking for GEOS support" ); - - if ( !conn->hasGEOS() ) - { - showMessageBox( tr( "No GEOS Support!" ), - tr( "Your PostGIS installation has no GEOS support.\n" - "Feature selection and identification will not " - "work properly.\nPlease install PostGIS with " - "GEOS support (http://geos.refractions.net)" ) ); - } - - if ( conn->hasTopology() ) - { - QgsDebugMsg( "Topology support available!" ); - } - - - - return conn; -} - -void QgsPostgresProvider::disconnectDb() -{ - if ( mFetching ) - { - connectionRO->closeCursor( QString( "qgisf%1" ).arg( providerId ) ); - mFetching = false; - } - - if ( connectionRO ) - { - Conn::disconnectRO( connectionRO ); - } - - if ( connectionRW ) - { - Conn::disconnectRW( connectionRW ); - } -} - -void QgsPostgresProvider::Conn::disconnectRW( Conn *&connection ) -{ - disconnect( connectionsRW, connection ); -} - -void QgsPostgresProvider::Conn::disconnectRO( Conn *&connection ) -{ - disconnect( connectionsRO, connection ); -} - -void QgsPostgresProvider::Conn::disconnect( QMap& connections, Conn *&conn ) -{ - QMap::iterator i; - for ( i = connections.begin(); i != connections.end() && i.value() != conn; i++ ) - ; - - assert( i.value() == conn ); - assert( i.value()->ref > 0 ); - - if ( --i.value()->ref == 0 ) - { - i.value()->PQfinish(); - delete i.value(); - connections.remove( i.key() ); - } - - conn = NULL; -} - -QStringList QgsPostgresProvider::pkCandidates( QString schemaName, QString viewName ) -{ - QStringList cols; - cols << QString::null; - - QString sql = QString( "select attname from pg_attribute join pg_type on atttypid=pg_type.oid WHERE pg_type.typname IN ('int2','int4','int8','oid') AND attrelid=regclass('\"%1\".\"%2\"')" ).arg( schemaName ).arg( viewName ); - QgsDebugMsg( sql ); - PGresult *colRes = connectionRO->PQexec( sql ); - - if ( PQresultStatus( colRes ) == PGRES_TUPLES_OK ) - { - for ( int i = 0; i < PQntuples( colRes ); i++ ) - { - QgsDebugMsg( PQgetvalue( colRes, i, 0 ) ); - cols << QString::fromUtf8( PQgetvalue( colRes, i, 0 ) ); - } - } - else - { - QgsMessageLog::logMessage( tr( "SQL:%1\nresult:%2\nerror:%3\n" ).arg( sql ).arg( PQresultStatus( colRes ) ).arg( PQresultErrorMessage( colRes ) ), tr( "PostgreSQL" ) ); - } - - PQclear( colRes ); - - return cols; -} - -bool QgsPostgresProvider::getTableInfo( bool searchGeometryColumnsOnly, bool searchPublicOnly, bool allowGeometrylessTables ) -{ - int nColumns = 0; - int nGTables = 0; - - PGresult *result = 0; - QgsPostgresLayerProperty layerProperty; - - QgsDebugMsg( "Entering." ); - - for ( int i = 0; i < 2; i++ ) - { - QString gtableName, columnName; - - if ( i == 0 ) - { - gtableName = "geometry_columns"; - columnName = "f_geometry_column"; - } - else if ( i == 1 ) - { - gtableName = "geography_columns"; - columnName = "f_geography_column"; - } - - // The following query returns only tables that exist and the user has SELECT privilege on. - // Can't use regclass here because table must exist, else error occurs. - QString sql = QString( "select " - "f_table_name," - "f_table_schema," - "%2," - "upper(type)," - "pg_class.relkind" - " from " - "%1," - "pg_class," - "pg_namespace" - " where " - "relname=f_table_name" - " and f_table_schema=nspname" - " and pg_namespace.oid=pg_class.relnamespace" - " and has_schema_privilege(pg_namespace.nspname,'usage')" - " and has_table_privilege('\"'||pg_namespace.nspname||'\".\"'||pg_class.relname||'\"','select')" // user has select privilege - " order by " - "f_table_schema,f_table_name,%2" ).arg( gtableName ).arg( columnName ); - - QgsDebugMsg( "sql: " + sql ); - - result = connectionRO->PQexec( sql ); - if ( result ) - { - if ( PQresultStatus( result ) != PGRES_TUPLES_OK ) - { - connectionRO->PQexecNR( "COMMIT" ); - } - else - { - nGTables++; - - if ( PQntuples( result ) > 0 ) - { - - for ( int idx = 0; idx < PQntuples( result ); idx++ ) - { - QString tableName = QString::fromUtf8( PQgetvalue( result, idx, 0 ) ); - QString schemaName = QString::fromUtf8( PQgetvalue( result, idx, 1 ) ); - QString column = QString::fromUtf8( PQgetvalue( result, idx, 2 ) ); - QString type = QString::fromUtf8( PQgetvalue( result, idx, 3 ) ); - QString relkind = QString::fromUtf8( PQgetvalue( result, idx, 4 ) ); - - QgsDebugMsg( QString( "%1 %2.%3.%4: %5 %6" ) - .arg( gtableName ) - .arg( schemaName ).arg( tableName ).arg( column ) - .arg( type ) - .arg( relkind ) ); - - layerProperty.type = type; - layerProperty.schemaName = schemaName; - layerProperty.tableName = tableName; - layerProperty.geometryColName = column; - layerProperty.pkCols = relkind == "v" ? pkCandidates( schemaName, tableName ) : QStringList(); - layerProperty.sql = ""; - - layersSupported.push_back( layerProperty ); - nColumns++; - } - } - } - } - - PQclear( result ); - result = 0; - } - - if ( nColumns == 0 ) - { - showMessageBox( tr( "Accessible tables could not be determined" ), - tr( "Database connection was successful, but the accessible tables could not be determined." ) ); - nColumns = -1; - } - - //search for geometry columns in tables that are not in the geometry_columns metatable - if ( !searchGeometryColumnsOnly ) - { - // Now have a look for geometry columns that aren't in the - // geometry_columns table. This code is specific to postgresql, - // but an equivalent query should be possible in other - // databases. - QString sql = "select " - "pg_class.relname" - ",pg_namespace.nspname" - ",pg_attribute.attname" - ",pg_class.relkind" - " from " - "pg_attribute" - ",pg_class" - ",pg_namespace" - " where " - "pg_namespace.oid=pg_class.relnamespace" - " and pg_attribute.attrelid = pg_class.oid" - " and (" - " exists (select * from pg_type WHERE pg_type.oid=pg_attribute.atttypid AND pg_type.typname IN ('geometry','geography','topogeometry'))" - " or pg_attribute.atttypid IN (select oid FROM pg_type a WHERE EXISTS (SELECT * FROM pg_type b WHERE a.typbasetype=b.oid AND b.typname IN ('geometry','geography','topogeometry')))" - ")" - " and has_schema_privilege( pg_namespace.nspname, 'usage' )" - " and has_table_privilege( '\"' || pg_namespace.nspname || '\".\"' || pg_class.relname || '\"', 'select' )"; - - // user has select privilege - if ( searchPublicOnly ) - sql += " and pg_namespace.nspname = 'public'"; - - if ( nColumns > 0 ) - { - // TODO: handle this for the topogeometry case - sql += " and not exists (select * from geometry_columns WHERE pg_namespace.nspname=f_table_schema AND pg_class.relname=f_table_name)"; - - if ( nGTables > 1 ) - { - // TODO: handle this for the topogeometry case - // TODO: handle this for the geometry case ? - sql += " and not exists (select * from geography_columns WHERE pg_namespace.nspname=f_table_schema AND pg_class.relname=f_table_name)"; - } - } - else - { - nColumns = 0; - } - - sql += " and pg_class.relkind in( 'v', 'r' )"; // only from views and relations (tables) - - QgsDebugMsg( "sql: " + sql ); - - result = connectionRO->PQexec( sql ); - - if ( PQresultStatus( result ) != PGRES_TUPLES_OK ) - { - showMessageBox( tr( "Accessible tables could not be determined" ), - tr( "Database connection was successful, but the accessible tables could not be determined.\n\n" - "The error message from the database was:\n%1\n" ) - .arg( QString::fromUtf8( PQresultErrorMessage( result ) ) ) ); - if ( nColumns == 0 ) - nColumns = -1; - } - else if ( PQntuples( result ) > 0 ) - { - for ( int i = 0; i < PQntuples( result ); i++ ) - { - // Have the column name, schema name and the table name. The concept of a - // catalog doesn't exist in postgresql so we ignore that, but we - // do need to get the geometry type. - - // Make the assumption that the geometry type for the first - // row is the same as for all other rows. - - QString table = QString::fromUtf8( PQgetvalue( result, i, 0 ) ); // relname - QString schema = QString::fromUtf8( PQgetvalue( result, i, 1 ) ); // nspname - QString column = QString::fromUtf8( PQgetvalue( result, i, 2 ) ); // attname - QString relkind = QString::fromUtf8( PQgetvalue( result, i, 3 ) ); // relation kind - - QgsDebugMsg( QString( "%1.%2.%3: %4" ).arg( schema ).arg( table ).arg( column ).arg( relkind ) ); - - layerProperty.type = QString::null; - layerProperty.schemaName = schema; - layerProperty.tableName = table; - layerProperty.geometryColName = column; - layerProperty.pkCols = relkind == "v" ? pkCandidates( schema, table ) : QStringList(); - layerProperty.sql = ""; - - layersSupported.push_back( layerProperty ); - nColumns++; - } - } - - PQclear( result ); - result = 0; - } - - if ( allowGeometrylessTables ) - { - QString sql = "select " - "pg_class.relname" - ",pg_namespace.nspname" - ",pg_class.relkind" - " from " - " pg_class" - ",pg_namespace" - " where " - "pg_namespace.oid=pg_class.relnamespace" - " and has_schema_privilege( pg_namespace.nspname, 'usage' )" - " and has_table_privilege( '\"' || pg_namespace.nspname || '\".\"' || pg_class.relname || '\"', 'select' )" - " and pg_class.relkind in( 'v', 'r' )"; - - // user has select privilege - if ( searchPublicOnly ) - sql += " and pg_namespace.nspname = 'public'"; - - QgsDebugMsg( "sql: " + sql ); - - result = connectionRO->PQexec( sql ); - - if ( PQresultStatus( result ) != PGRES_TUPLES_OK ) - { - showMessageBox( tr( "Accessible tables could not be determined" ), - tr( "Database connection was successful, but the accessible tables could not be determined.\n\n" - "The error message from the database was:\n%1\n" ) - .arg( QString::fromUtf8( PQresultErrorMessage( result ) ) ) ); - if ( nColumns == 0 ) - nColumns = -1; - } - else if ( PQntuples( result ) > 0 ) - { - for ( int i = 0; i < PQntuples( result ); i++ ) - { - QString table = QString::fromUtf8( PQgetvalue( result, i, 0 ) ); // relname - QString schema = QString::fromUtf8( PQgetvalue( result, i, 1 ) ); // nspname - QString relkind = QString::fromUtf8( PQgetvalue( result, i, 2 ) ); // relation kind - - QgsDebugMsg( QString( "%1.%2: %3" ).arg( schema ).arg( table ).arg( relkind ) ); - - layerProperty.type = QString::null; - layerProperty.schemaName = schema; - layerProperty.tableName = table; - layerProperty.geometryColName = QString::null; - layerProperty.pkCols = relkind == "v" ? pkCandidates( schema, table ) : QStringList(); - layerProperty.sql = ""; - - layersSupported.push_back( layerProperty ); - nColumns++; - } - } - - PQclear( result ); - result = 0; - } - - if ( nColumns == 0 ) - { - showMessageBox( tr( "No accessible tables found" ), - tr( "Database connection was successful, but no accessible tables were found.\n\n" - "Please verify that you have SELECT privilege on a table carrying PostGIS\n" - "geometry." ) ); - } - - return nColumns > 0; -} - -bool QgsPostgresProvider::supportedLayers( QVector &layers, - bool searchGeometryColumnsOnly, - bool searchPublicOnly, - bool allowGeometrylessTables ) -{ - QgsDebugMsg( "Entering." ); - - // Open the connection - if ( connectionRO == NULL ) - { - connectionRO = Conn::connectDb( mUri.connectionInfo(), true ); - if ( connectionRO == NULL ) - { - return false; - } - } - QgsDebugMsg( "before getTableInfo." ); - - // Get the list of supported tables - if ( !getTableInfo( searchGeometryColumnsOnly, searchPublicOnly, allowGeometrylessTables ) ) - { - QgsMessageLog::logMessage( tr( "Unable to get list of spatially enabled tables from the database" ), tr( "PostgreSQL" ) ); - return false; - } - - layers = layersSupported; - - QgsDebugMsg( "Exiting." ); - - return true; -} - - -QString QgsPostgresProvider::storageType() const -{ - return "PostgreSQL database with PostGIS extension"; -} - -QString QgsPostgresProvider::fieldExpression( const QgsField &fld ) const -{ - const QString &type = fld.typeName(); - if ( type == "money" ) - { - return QString( "cash_out(%1)" ).arg( quotedIdentifier( fld.name() ) ); - } - else if ( type.startsWith( "_" ) ) - { - return QString( "array_out(%1)" ).arg( quotedIdentifier( fld.name() ) ); - } - else if ( type == "bool" ) - { - return QString( "boolout(%1)" ).arg( quotedIdentifier( fld.name() ) ); - } - else if ( type == "geometry" ) - { - return QString( "%1(%2)" ) - .arg( connectionRO->majorVersion() < 2 ? "asewkt" : "st_asewkt" ) - .arg( quotedIdentifier( fld.name() ) ); - } - else if ( type == "geography" ) - { - return QString( "st_astext(%1)" ).arg( quotedIdentifier( fld.name() ) ); - } - else - { - return quotedIdentifier( fld.name() ) + "::text"; - } -} - -bool QgsPostgresProvider::declareCursor( - const QString &cursorName, - const QgsAttributeList &fetchAttributes, - bool fetchGeometry, - QString whereClause ) -{ - if ( fetchGeometry && geometryColumn.isNull() ) - { - return false; - } - - try - { - QString query = QString( "select %1" ).arg( quotedIdentifier( primaryKey ) ); - - if ( fetchGeometry ) - { - if ( isGeography ) - { - query += QString( ",st_asbinary(%1)" ) - .arg( quotedIdentifier( geometryColumn ) ); - } - else - { - query += QString( ",%1(%2,'%3')" ) - .arg( connectionRO->majorVersion() < 2 ? "asbinary" : "st_asbinary" ) - .arg( quotedIdentifier( geometryColumn ) ) - .arg( endianString() ); - } - } - - for ( QgsAttributeList::const_iterator it = fetchAttributes.constBegin(); it != fetchAttributes.constEnd(); ++it ) - { - const QgsField &fld = field( *it ); - - if ( fld.name() == primaryKey ) - continue; - - query += "," + fieldExpression( fld ); - } - - query += " from " + mQuery; - - if ( !whereClause.isEmpty() ) - query += QString( " where %1" ).arg( whereClause ); - - if ( !connectionRO->openCursor( cursorName, query ) ) - { - // reloading the fields might help next time around - rewind(); - return false; - } - } - catch ( PGFieldNotFound ) - { - return false; - } - - return true; -} - -qint64 QgsPostgresProvider::getBinaryInt( PGresult *queryResult, int row, int col ) -{ - qint64 oid; - char *p = PQgetvalue( queryResult, row, col ); - size_t s = PQgetlength( queryResult, row, col ); - -#ifdef QGISDEBUG - QString buf = ""; - for ( size_t i = 0; i < s; i++ ) - { - buf += QString( "%1 " ).arg( *( unsigned char * )( p + i ), 0, 16, QLatin1Char( ' ' ) ); - } - - QgsDebugMsgLevel( QString( "int in hex:%1" ).arg( buf ), 4 ); -#endif - - switch ( s ) - { - case 2: - oid = *( qint16 * )p; - if ( swapEndian ) - oid = ntohs( oid ); - break; - - case 6: - { - qint64 block = *( qint32 * ) p; - qint64 offset = *( qint16 * )( p + sizeof( qint32 ) ); - - if ( swapEndian ) - { - block = ntohl( block ); - offset = ntohs( offset ); - } - - oid = ( block << 16 ) + offset; - } - break; - - case 8: - { - qint32 oid0 = *( qint32 * ) p; - qint32 oid1 = *( qint32 * )( p + sizeof( qint32 ) ); - - if ( swapEndian ) - { - QgsDebugMsgLevel( QString( "swap oid0:%1 oid1:%2" ).arg( oid0 ).arg( oid1 ), 4 ); - oid0 = ntohl( oid0 ); - oid1 = ntohl( oid1 ); - } - - QgsDebugMsgLevel( QString( "oid0:%1 oid1:%2" ).arg( oid0 ).arg( oid1 ), 4 ); - oid = oid0; - QgsDebugMsgLevel( QString( "oid:%1" ).arg( oid ), 4 ); - oid <<= 32; - QgsDebugMsgLevel( QString( "oid:%1" ).arg( oid ), 4 ); - oid |= oid1; - QgsDebugMsgLevel( QString( "oid:%1" ).arg( oid ), 4 ); - } - break; - - default: - QgsDebugMsg( QString( "unexpected size %d" ).arg( s ) ); - - case 4: - oid = *( qint32 * )p; - if ( swapEndian ) - oid = ntohl( oid ); - break; - } - - return oid; -} - -bool QgsPostgresProvider::getFeature( PGresult *queryResult, int row, bool fetchGeometry, - QgsFeature &feature, - const QgsAttributeList &fetchAttributes ) -{ - try - { - QgsFeatureId oid = getBinaryInt( queryResult, row, 0 ); - QgsDebugMsgLevel( QString( "oid=%1" ).arg( oid ), 4 ); - - feature.setFeatureId( oid ); - feature.clearAttributeMap(); - - int col; // first attribute column after geometry - - if ( fetchGeometry ) - { - int returnedLength = PQgetlength( queryResult, row, 1 ); - if ( returnedLength > 0 ) - { - unsigned char *featureGeom = new unsigned char[returnedLength + 1]; - memset( featureGeom, '\0', returnedLength + 1 ); - memcpy( featureGeom, PQgetvalue( queryResult, row, 1 ), returnedLength ); - feature.setGeometryAndOwnership( featureGeom, returnedLength + 1 ); - } - else - { - feature.setGeometryAndOwnership( 0, 0 ); - QgsMessageLog::logMessage( tr( "Couldn't get the feature geometry in binary form" ), tr( "PostgreSQL" ) ); - } - - col = 2; - } - else - { - col = 1; - } - - // iterate attributes - for ( QgsAttributeList::const_iterator it = fetchAttributes.constBegin(); it != fetchAttributes.constEnd(); it++ ) - { - const QgsField &fld = field( *it ); - - if ( fld.name() == primaryKey ) - { - // primary key was already processed - feature.addAttribute( *it, convertValue( fld.type(), FID_TO_STRING( oid ) ) ); - continue; - } - - if ( !PQgetisnull( queryResult, row, col ) ) - { - feature.addAttribute( *it, convertValue( fld.type(), QString::fromUtf8( PQgetvalue( queryResult, row, col ) ) ) ); - } - else - { - feature.addAttribute( *it, QVariant( QString::null ) ); - } - - col++; - } - - return true; - } - catch ( PGFieldNotFound ) - { - return false; - } -} - -void QgsPostgresProvider::select( QgsAttributeList fetchAttributes, QgsRectangle rect, bool fetchGeometry, bool useIntersect ) -{ - QString cursorName = QString( "qgisf%1" ).arg( providerId ); - - if ( mFetching ) - { - connectionRO->closeCursor( cursorName ); - mFetching = false; - - while ( !mFeatureQueue.empty() ) - { - mFeatureQueue.pop(); - } - } - - QString whereClause; - - if ( !rect.isEmpty() && !geometryColumn.isNull() ) - { - if ( isGeography ) - { - rect = QgsRectangle( -180.0, -90.0, 180.0, 90.0 ).intersect( &rect ); - if ( !rect.isFinite() ) - whereClause = "false"; - } - - if ( whereClause.isEmpty() ) - { - QString qBox = QString( "%1('BOX3D(%2)'::box3d,%3)" ) - .arg( connectionRO->majorVersion() < 2 ? "setsrid" : "st_setsrid" ) - .arg( rect.asWktCoordinates() ) - .arg( srid ); - whereClause = QString( "%1 && %2" ) - .arg( quotedIdentifier( geometryColumn ) ) - .arg( qBox ); - if ( useIntersect ) - { - whereClause += QString( " and %1(%2,%3)" ) - .arg( connectionRO->majorVersion() < 2 ? "intersects" : "st_intersects" ) - .arg( quotedIdentifier( geometryColumn ) ) - .arg( qBox ); - } - } - } - - if ( !sqlWhereClause.isEmpty() ) - { - if ( !whereClause.isEmpty() ) - whereClause += " and "; - - whereClause += "(" + sqlWhereClause + ")"; - } - - mFetchGeom = fetchGeometry; - mAttributesToFetch = fetchAttributes; - if ( !declareCursor( cursorName, fetchAttributes, fetchGeometry, whereClause ) ) - return; - - mFetching = true; - mFetched = 0; -} - -bool QgsPostgresProvider::nextFeature( QgsFeature& feature ) -{ - feature.setValid( false ); - if ( !valid ) - { - QgsMessageLog::logMessage( tr( "Read attempt on an invalid postgresql data source" ), tr( "PostgreSQL" ) ); - return false; - } - - if ( !mFetching ) - { - QgsMessageLog::logMessage( tr( "nextFeature() without select()" ), tr( "PostgreSQL" ) ); - return false; - } - - QString cursorName = QString( "qgisf%1" ).arg( providerId ); - - if ( mFeatureQueue.empty() ) - { - QString fetch = QString( "fetch forward %1 from %2" ).arg( mFeatureQueueSize ).arg( cursorName ); - if ( connectionRO->PQsendQuery( fetch ) == 0 ) // fetch features asynchronously - { - QgsMessageLog::logMessage( tr( "fetching from cursor %1 failed\nDatabase error: %2" ).arg( cursorName ).arg( QString::fromUtf8( PQerrorMessage( pgConnection() ) ) ), tr( "PostgreSQL" ) ); - } - - Result queryResult; - while (( queryResult = connectionRO->PQgetResult() ) ) - { - int rows = PQntuples( queryResult ); - if ( rows == 0 ) - continue; - - for ( int row = 0; row < rows; row++ ) - { - mFeatureQueue.push( QgsFeature() ); - getFeature( queryResult, row, mFetchGeom, mFeatureQueue.back(), mAttributesToFetch ); - } // for each row in queue - } - } - - if ( mFeatureQueue.empty() ) - { - QgsDebugMsg( QString( "finished after %1 features" ).arg( mFetched ) ); - connectionRO->closeCursor( cursorName ); - mFetching = false; - if ( featuresCounted < mFetched ) - { - QgsDebugMsg( QString( "feature count adjusted from %1 to %2" ).arg( featuresCounted ).arg( mFetched ) ); - featuresCounted = mFetched; - } - return false; - } - - // Now return the next feature from the queue - if ( mFetchGeom ) - { - QgsGeometry* featureGeom = mFeatureQueue.front().geometryAndOwnership(); - feature.setGeometry( featureGeom ); - } - else - { - feature.setGeometryAndOwnership( 0, 0 ); - } - feature.setFeatureId( mFeatureQueue.front().id() ); - feature.setAttributeMap( mFeatureQueue.front().attributeMap() ); - - mFeatureQueue.pop(); - mFetched++; - - feature.setValid( true ); - return true; -} - -QString QgsPostgresProvider::whereClause( QgsFeatureId featureId ) const -{ - QString whereClause; - - if ( primaryKeyType != "tid" ) - { - whereClause = QString( "%1=%2" ).arg( quotedIdentifier( primaryKey ) ).arg( featureId ); - } - else - { - whereClause = QString( "%1='(%2,%3)'" ) - .arg( quotedIdentifier( primaryKey ) ) - .arg( FID_TO_NUMBER( featureId ) >> 16 ) - .arg( FID_TO_NUMBER( featureId ) & 0xffff ); - } - - if ( !sqlWhereClause.isEmpty() ) - { - if ( !whereClause.isEmpty() ) - whereClause += " and "; - - whereClause += "(" + sqlWhereClause + ")"; - } - - return whereClause; -} - -bool QgsPostgresProvider::featureAtId( QgsFeatureId featureId, QgsFeature& feature, bool fetchGeometry, QgsAttributeList fetchAttributes ) -{ - feature.setValid( false ); - -#if 0 - if ( mFeatureMap.contains( featureId ) ) - { - QgsFeature * fpointer = &feature; - *fpointer = mFeatureMap.value( featureId ); - QgsDebugMsg( QString( "retrieve feature %1 from cache" ).arg( featureId ) ); - - mPriorityIds.removeAll( featureId ); - mPriorityIds.prepend( featureId ); - return true; - } -#endif - - QString cursorName = QString( "qgisfid%1" ).arg( providerId ); - - if ( !declareCursor( cursorName, fetchAttributes, fetchGeometry, whereClause( featureId ) ) ) - return false; - - Result queryResult = connectionRO->PQexec( QString( "fetch forward 1 from %1" ).arg( cursorName ) ); - if ( queryResult == 0 ) - return false; - - int rows = PQntuples( queryResult ); - if ( rows == 0 ) - { - QgsMessageLog::logMessage( tr( "feature %1 not found" ).arg( featureId ), tr( "PostgreSQL" ) ); - connectionRO->closeCursor( cursorName ); - return false; - } - else if ( rows != 1 ) - { - QgsMessageLog::logMessage( tr( "found %1 features instead of just one." ).arg( rows ), tr( "PostgreSQL" ) ); - } - - bool gotit = getFeature( queryResult, 0, fetchGeometry, feature, fetchAttributes ); - - connectionRO->closeCursor( cursorName ); - - feature.setValid( gotit ); - -#if 0 - if ( gotit ) - { - mFeatureMap.insert( featureId, feature ); - mPriorityIds.prepend( featureId ); - if ( mPriorityIds.count() == 20 ) - { - mFeatureMap.remove( mPriorityIds.takeLast() ); - } - } -#endif - - return gotit; -} - - -QgsDataSourceURI& QgsPostgresProvider::getURI() -{ - return mUri; -} - -void QgsPostgresProvider::setExtent( QgsRectangle& newExtent ) -{ - layerExtent.setXMaximum( newExtent.xMaximum() ); - layerExtent.setXMinimum( newExtent.xMinimum() ); - layerExtent.setYMaximum( newExtent.yMaximum() ); - layerExtent.setYMinimum( newExtent.yMinimum() ); -} - -/** - * Return the feature type - */ -QGis::WkbType QgsPostgresProvider::geometryType() const -{ - return geomType; -} - -const QgsField &QgsPostgresProvider::field( int index ) const -{ - QgsFieldMap::const_iterator it = attributeFields.find( index ); - - if ( it == attributeFields.constEnd() ) - { - QgsLogger::warning( "Field " + QString::number( index ) + " not found." ); - throw PGFieldNotFound(); - } - - return it.value(); -} - -/** - * Return the number of fields - */ -uint QgsPostgresProvider::fieldCount() const -{ - return attributeFields.size(); -} - -const QgsFieldMap & QgsPostgresProvider::fields() const -{ - return attributeFields; -} - -QString QgsPostgresProvider::dataComment() const -{ - return mDataComment; -} - -void QgsPostgresProvider::rewind() -{ - if ( mFetching ) - { - //move cursor to first record - connectionRO->PQexecNR( QString( "move 0 in qgisf%1" ).arg( providerId ) ); - } - mFeatureQueue.empty(); - loadFields(); -} - -/** @todo XXX Perhaps this should be promoted to QgsDataProvider? */ -QString QgsPostgresProvider::endianString() -{ - switch ( QgsApplication::endian() ) - { - case QgsApplication::NDR: - return QString( "NDR" ); - break; - case QgsApplication::XDR: - return QString( "XDR" ); - break; - default : - return QString( "Unknown" ); - } -} - -bool QgsPostgresProvider::loadFields() -{ - if ( !isQuery ) - { - QgsDebugMsg( "Loading fields for table " + mTableName ); - - // Get the relation oid for use in later queries - QString sql = QString( "SELECT regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); - Result tresult = connectionRO->PQexec( sql ); - QString tableoid = QString::fromUtf8( PQgetvalue( tresult, 0, 0 ) ); - - // Get the table description - sql = QString( "SELECT description FROM pg_description WHERE objoid=%1 AND objsubid=0" ).arg( tableoid ); - tresult = connectionRO->PQexec( sql ); - if ( PQntuples( tresult ) > 0 ) - mDataComment = QString::fromUtf8( PQgetvalue( tresult, 0, 0 ) ); - } - - // Populate the field vector for this layer. The field vector contains - // field name, type, length, and precision (if numeric) - QString sql = QString( "select * from %1 limit 0" ).arg( mQuery ); - - Result result = connectionRO->PQexec( sql ); - - QSet fields; - - // The queries inside this loop could possibly be combined into one - // single query - this would make the code run faster. - attributeFields.clear(); - for ( int i = 0; i < PQnfields( result ); i++ ) - { - QString fieldName = QString::fromUtf8( PQfname( result, i ) ); - if ( fieldName == geometryColumn ) - continue; - - int fldtyp = PQftype( result, i ); - QString typOid = QString().setNum( fldtyp ); - int fieldPrec = -1; - QString fieldComment( "" ); - int tableoid = PQftable( result, i ); - - sql = QString( "SELECT typname,typtype,typelem,typlen FROM pg_type WHERE oid=%1" ).arg( typOid ); - // just oid; needs more work to support array type - // "oid = (SELECT Distinct typelem FROM pg_type WHERE " //needs DISTINCT to guard against 2 or more rows on int2 - // "typelem = " + typOid + " AND typlen = -1)"; - - Result oidResult = connectionRO->PQexec( sql ); - QString fieldTypeName = QString::fromUtf8( PQgetvalue( oidResult, 0, 0 ) ); - QString fieldTType = QString::fromUtf8( PQgetvalue( oidResult, 0, 1 ) ); - QString fieldElem = QString::fromUtf8( PQgetvalue( oidResult, 0, 2 ) ); - int fieldSize = QString::fromUtf8( PQgetvalue( oidResult, 0, 3 ) ).toInt(); - - QString formattedFieldType; - if ( tableoid > 0 ) - { - sql = QString( "SELECT attnum,pg_catalog.format_type(atttypid,atttypmod) FROM pg_attribute WHERE attrelid=%1 AND attname=%2" ) - .arg( tableoid ).arg( quotedValue( fieldName ) ); - - Result tresult = connectionRO->PQexec( sql ); - QString attnum = QString::fromUtf8( PQgetvalue( tresult, 0, 0 ) ); - formattedFieldType = QString::fromUtf8( PQgetvalue( tresult, 0, 1 ) ); - - sql = QString( "SELECT description FROM pg_description WHERE objoid=%1 AND objsubid=%2" ) - .arg( tableoid ).arg( attnum ); - - tresult = connectionRO->PQexec( sql ); - if ( PQntuples( tresult ) > 0 ) - fieldComment = QString::fromUtf8( PQgetvalue( tresult, 0, 0 ) ); - } - - QVariant::Type fieldType; - - if ( fieldTType == "b" ) - { - bool isArray = fieldTypeName.startsWith( "_" ); - - if ( isArray ) - fieldTypeName = fieldTypeName.mid( 1 ); - - if ( fieldTypeName == "int8" ) - { - fieldType = QVariant::LongLong; - fieldSize = -1; - fieldPrec = 0; - } - else if ( fieldTypeName.startsWith( "int" ) || - fieldTypeName == "serial" ) - { - fieldType = QVariant::Int; - fieldSize = -1; - fieldPrec = 0; - } - else if ( fieldTypeName == "real" || - fieldTypeName == "double precision" || - fieldTypeName.startsWith( "float" ) ) - { - fieldType = QVariant::Double; - fieldSize = -1; - fieldPrec = -1; - } - else if ( fieldTypeName == "numeric" ) - { - fieldType = QVariant::Double; - - if ( formattedFieldType == "numeric" ) - { - fieldSize = -1; - fieldPrec = -1; - } - else - { - QRegExp re( "numeric\\((\\d+),(\\d+)\\)" ); - if ( re.exactMatch( formattedFieldType ) ) - { - fieldSize = re.cap( 1 ).toInt(); - fieldPrec = re.cap( 2 ).toInt(); - } - else if ( formattedFieldType != "numeric" ) - { - QgsMessageLog::logMessage( tr( "unexpected formatted field type '%1' for field %2" ) - .arg( formattedFieldType ) - .arg( fieldName ), - tr( "PostgreSQL" ) ); - fieldSize = -1; - fieldPrec = -1; - } - } - } - else if ( fieldTypeName == "text" || - fieldTypeName == "bpchar" || - fieldTypeName == "varchar" || - fieldTypeName == "bool" || - fieldTypeName == "geometry" || - fieldTypeName == "money" || - fieldTypeName == "ltree" || - fieldTypeName == "uuid" || - fieldTypeName.startsWith( "time" ) || - fieldTypeName.startsWith( "date" ) ) - { - fieldType = QVariant::String; - fieldSize = -1; - } - else if ( fieldTypeName == "char" ) - { - fieldType = QVariant::String; - - QRegExp re( "char\\((\\d+)\\)" ); - if ( re.exactMatch( formattedFieldType ) ) - { - fieldSize = re.cap( 1 ).toInt(); - } - else - { - QgsMessageLog::logMessage( tr( "unexpected formatted field type '%1' for field %2" ) - .arg( formattedFieldType ) - .arg( fieldName ) ); - fieldSize = -1; - fieldPrec = -1; - } - } - else - { - QgsLogger::warning( "Field " + fieldName + " ignored, because of unsupported type " + fieldTypeName ); - continue; - } - - if ( isArray ) - { - fieldTypeName = "_" + fieldTypeName; - fieldType = QVariant::String; - fieldSize = -1; - } - } - else if ( fieldTType == "e" ) - { - // enum - fieldType = QVariant::String; - fieldSize = -1; - } - else - { - QgsLogger::warning( "Field " + fieldName + " ignored, because of unsupported type type " + fieldTType ); - continue; - } - - if ( fields.contains( fieldName ) ) - { - showMessageBox( tr( "Ambiguous field!" ), - tr( "Duplicate field %1 found\n" ).arg( fieldName ) ); - return false; - } - - fields << fieldName; - - attributeFields.insert( i, QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment ) ); - } - - return true; -} - -bool QgsPostgresProvider::hasSufficientPermsAndCapabilities() -{ - QgsDebugMsg( "Checking for permissions on the relation" ); - - Result testAccess; - if ( !isQuery ) - { - // Check that we can read from the table (i.e., we have - // select permission). - QString sql = QString( "select * from %1 limit 1" ).arg( mQuery ); - Result testAccess = connectionRO->PQexec( sql ); - if ( PQresultStatus( testAccess ) != PGRES_TUPLES_OK ) - { - showMessageBox( tr( "Unable to access relation" ), - tr( "Unable to access the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" ) - .arg( mQuery ) - .arg( QString::fromUtf8( PQresultErrorMessage( testAccess ) ) ) - .arg( sql ) ); - return false; - } - - bool inRecovery = false; - - if ( connectionRO->pgVersion() >= 90000 ) - { - testAccess = connectionRO->PQexec( "SELECT pg_is_in_recovery()" ); - if ( PQresultStatus( testAccess ) != PGRES_TUPLES_OK || QString::fromUtf8( PQgetvalue( testAccess, 0, 0 ) ) == "t" ) - { - showMessageBox( tr( "PostgreSQL in recovery" ), - tr( "PostgreSQL is still in recovery after a database crash\n(or you are connected to a (read-only) slave).\nWrite accesses will be denied." ) ); - inRecovery = true; - } - } - - // postgres has fast access to features at id (thanks to primary key / unique index) - // the latter flag is here just for compatibility - if ( !mSelectAtIdDisabled ) - { - enabledCapabilities = QgsVectorDataProvider::SelectAtId | QgsVectorDataProvider::SelectGeometryAtId; - } - - if ( !inRecovery ) - { - if ( connectionRO->pgVersion() >= 80400 ) - { - sql = QString( "SELECT " - "has_table_privilege(%1,'DELETE')," - "has_any_column_privilege(%1,'UPDATE')," - "%2" - "has_table_privilege(%1,'INSERT')," - "current_schema()" ) - .arg( quotedValue( mQuery ) ) - .arg( geometryColumn.isNull() - ? QString( "'f'," ) - : QString( "has_column_privilege(%1,%2,'UPDATE')," ) - .arg( quotedValue( mQuery ) ) - .arg( quotedValue( geometryColumn ) ) - ); - } - else - { - sql = QString( "SELECT " - "has_table_privilege(%1,'DELETE')," - "has_table_privilege(%1,'UPDATE')," - "has_table_privilege(%1,'UPDATE')," - "has_table_privilege(%1,'INSERT')," - "current_schema()" ) - .arg( quotedValue( mQuery ) ); - } - - testAccess = connectionRO->PQexec( sql ); - if ( PQresultStatus( testAccess ) != PGRES_TUPLES_OK ) - { - showMessageBox( tr( "Unable to access relation" ), - tr( "Unable to determine table access privileges for the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" ) - .arg( mQuery ) - .arg( QString::fromUtf8( PQresultErrorMessage( testAccess ) ) ) - .arg( sql ) ); - return false; - } - - - if ( QString::fromUtf8( PQgetvalue( testAccess, 0, 0 ) ) == "t" ) - { - // DELETE - enabledCapabilities |= QgsVectorDataProvider::DeleteFeatures; - } - - if ( QString::fromUtf8( PQgetvalue( testAccess, 0, 1 ) ) == "t" ) - { - // UPDATE - enabledCapabilities |= QgsVectorDataProvider::ChangeAttributeValues; - } - - if ( QString::fromUtf8( PQgetvalue( testAccess, 0, 2 ) ) == "t" ) - { - // UPDATE - enabledCapabilities |= QgsVectorDataProvider::ChangeGeometries; - } - - if ( QString::fromUtf8( PQgetvalue( testAccess, 0, 3 ) ) == "t" ) - { - // INSERT - enabledCapabilities |= QgsVectorDataProvider::AddFeatures; - } - - mCurrentSchema = QString::fromUtf8( PQgetvalue( testAccess, 0, 4 ) ); - if ( mCurrentSchema == mSchemaName ) - { - mUri.clearSchema(); - } - - if ( mSchemaName == "" ) - mSchemaName = mCurrentSchema; - - sql = QString( "SELECT 1 FROM pg_class,pg_namespace WHERE " - "pg_class.relnamespace=pg_namespace.oid AND " - "pg_get_userbyid(relowner)=current_user AND " - "relname=%1 AND nspname=%2" ) - .arg( quotedValue( mTableName ) ) - .arg( quotedValue( mSchemaName ) ); - testAccess = connectionRO->PQexec( sql ); - if ( PQresultStatus( testAccess ) == PGRES_TUPLES_OK && PQntuples( testAccess ) == 1 ) - { - enabledCapabilities |= QgsVectorDataProvider::AddAttributes | QgsVectorDataProvider::DeleteAttributes; - } - } - } - else - { - // Check if the sql is a select query - if ( !mQuery.startsWith( "(select", Qt::CaseInsensitive ) && - !mQuery.endsWith( ")" ) ) - { - QgsMessageLog::logMessage( tr( "The custom query is not a select query." ), tr( "PostgreSQL" ) ); - return false; - } - - // get a new alias for the subquery - int index = 0; - QString alias; - QRegExp regex; - do - { - alias = QString( "subQuery_%1" ).arg( QString::number( index++ ) ); - QString pattern = QString( "(\\\"?)%1\\1" ).arg( QRegExp::escape( alias ) ); - regex.setPattern( pattern ); - regex.setCaseSensitivity( Qt::CaseInsensitive ); - } - while ( mQuery.contains( regex ) ); - - // convert the custom query into a subquery - mQuery = QString( "%1 as %2" ) - .arg( mQuery ) - .arg( quotedIdentifier( alias ) ); - - QString sql = QString( "select * from %1 limit 1" ).arg( mQuery ); - - testAccess = connectionRO->PQexec( sql ); - if ( PQresultStatus( testAccess ) != PGRES_TUPLES_OK ) - { - showMessageBox( tr( "Unable execute the query" ), - tr( "Unable to execute the query.\nThe error message from the database was:\n%1.\nSQL: %2" ) - .arg( QString::fromUtf8( PQresultErrorMessage( testAccess ) ) ) - .arg( sql ) ); - return false; - } - - if ( !mSelectAtIdDisabled ) - { - enabledCapabilities = QgsVectorDataProvider::SelectAtId | QgsVectorDataProvider::SelectGeometryAtId; - } - } - - return true; -} - -QString QgsPostgresProvider::getPrimaryKey() -{ - // If we find a database primary key we will set this to true. If it is a column which is serving - // as a primary key, then this will remain false. - mIsDbPrimaryKey = false; - - // check to see if there is an unique index on the relation, which - // can be used as a key into the table. Primary keys are always - // unique indices, so we catch them as well. - - QString sql; - if ( !isQuery ) - { - sql = QString( "select indkey from pg_index where indisunique and indrelid=regclass(%1)::oid and indpred is null" ) - .arg( quotedValue( mQuery ) ); - - QgsDebugMsg( "Getting unique index using '" + sql + "'" ); - - Result pk = connectionRO->PQexec( sql ); - - QgsDebugMsg( "Got " + QString::number( PQntuples( pk ) ) + " rows." ); - - QStringList log; - - // if we got no tuples we ain't got no unique index :) - if ( PQntuples( pk ) == 0 ) - { - QgsDebugMsg( "Relation has no unique index -- investigating alternatives" ); - - // Two options here. If the relation is a table, see if there is - // an oid column that can be used instead. - // If the relation is a view try to find a suitable column to use as - // the primary key. - - sql = QString( "SELECT relkind FROM pg_class WHERE oid=regclass(%1)::oid" ) - .arg( quotedValue( mQuery ) ); - Result tableType = connectionRO->PQexec( sql ); - QString type = QString::fromUtf8( PQgetvalue( tableType, 0, 0 ) ); - - if ( type == "r" ) // the relation is a table - { - QgsDebugMsg( "Relation is a table. Checking to see if it has an oid column." ); - - primaryKey = ""; - - // If there is an oid on the table, use that instead, - // otherwise give up - sql = QString( "SELECT attname FROM pg_attribute WHERE attname='oid' AND attrelid=regclass(%1)" ) - .arg( quotedValue( mQuery ) ); - - Result oidCheck = connectionRO->PQexec( sql ); - - if ( PQntuples( oidCheck ) != 0 ) - { - // Could warn the user here that performance will suffer if - // oid isn't indexed (and that they may want to add a - // primary key to the table) - primaryKey = "oid"; - primaryKeyType = "oid"; - mIsDbPrimaryKey = true; - } - else - { - sql = QString( "SELECT attname FROM pg_attribute WHERE attname='ctid' AND attrelid=regclass(%1)" ) - .arg( quotedValue( mQuery ) ); - - Result ctidCheck = connectionRO->PQexec( sql ); - if ( PQntuples( ctidCheck ) == 1 ) - { - primaryKey = "ctid"; - primaryKeyType = "tid"; - mIsDbPrimaryKey = true; - } - } - - if ( primaryKey.isEmpty() ) - { - showMessageBox( tr( "No suitable key column in table" ), - tr( "The table has no column suitable for use as a key.\n\n" - "Quantum GIS requires that the table either has a column of type\n" - "integer with an unique constraint on it (which includes the\n" - "primary key), has a PostgreSQL oid column or has a ctid\n" - "column.\n" ) ); - } - else - { - mPrimaryKeyDefault = defaultValue( primaryKey ).toString(); - if ( mPrimaryKeyDefault.isNull() ) - { - mPrimaryKeyDefault = QString( "max(%1)+1 from %2.%3" ) - .arg( quotedIdentifier( primaryKey ) ) - .arg( quotedIdentifier( mSchemaName ) ) - .arg( quotedIdentifier( mTableName ) ); - } - } - } - else if ( type == "v" ) // the relation is a view - { - if ( !primaryKey.isEmpty() ) - { - // check last used candidate - sql = QString( "select pg_type.typname from pg_attribute,pg_type where atttypid=pg_type.oid and attname=%1 and attrelid=regclass(%2)" ) - .arg( quotedValue( primaryKey ) ).arg( quotedValue( mQuery ) ); - - QgsDebugMsg( "checking candidate: " + sql ); - - Result result = connectionRO->PQexec( sql ); - - QString type; - if ( PQresultStatus( result ) == PGRES_TUPLES_OK && - PQntuples( result ) == 1 ) - { - type = PQgetvalue( result, 0, 0 ); - } - - // mPrimaryKeyDefault stays null and is retrieved later on demand - // if mUseEstimatedMetadata is on assume that the already keyfield is still unique - - if (( type != "int2" && type != "int4" && type != "int8" && type != "oid" ) || - ( !mUseEstimatedMetadata && !uniqueData( mQuery, primaryKey ) ) ) - { - primaryKey = ""; - } - } - - if ( primaryKey.isEmpty() ) - { - parseView(); - } - } - else - { - QgsMessageLog::logMessage( tr( "Unexpected relation type of '%1'." ).arg( type ), tr( "PostgreSQL" ) ); - } - } - else // have some unique indices on the table. Now choose one... - { - // choose which (if more than one) unique index to use - std::vector > suitableKeyColumns; - for ( int i = 0; i < PQntuples( pk ); ++i ) - { - QString col = QString::fromUtf8( PQgetvalue( pk, i, 0 ) ); - QStringList columns = col.split( " ", QString::SkipEmptyParts ); - if ( columns.count() == 1 ) - { - // Get the column name and data type - sql = QString( "select attname,pg_type.typname from pg_attribute,pg_type where atttypid=pg_type.oid and attnum=%1 and attrelid=regclass(%2)" ) - .arg( col ).arg( quotedValue( mQuery ) ); - Result types = connectionRO->PQexec( sql ); - - if ( PQntuples( types ) > 0 ) - { - QString columnName = QString::fromUtf8( PQgetvalue( types, 0, 0 ) ); - QString columnType = QString::fromUtf8( PQgetvalue( types, 0, 1 ) ); - - if ( columnType != "int2" && columnType != "int4" && columnType != "int8" ) - log.append( tr( "The unique index on column '%1' is unsuitable because Quantum GIS does not currently " - "support non-integer typed columns as a key into the table.\n" ).arg( columnName ) ); - else - { - mIsDbPrimaryKey = true; - suitableKeyColumns.push_back( std::make_pair( columnName, columnType ) ); - } - } - else - { - //QgsDebugMsg( QString("name and type of %3. column of %1.%2 not found").arg(mSchemaName).arg(mTables).arg(col) ); - } - } - else - { - sql = QString( "select attname from pg_attribute, pg_type where atttypid=pg_type.oid and attnum in (%1) and attrelid=regclass(%2)::oid" ) - .arg( col.replace( " ", "," ) ) - .arg( quotedValue( mQuery ) ); - - Result types = connectionRO->PQexec( sql ); - QString colNames; - int numCols = PQntuples( types ); - for ( int j = 0; j < numCols; ++j ) - { - if ( j == numCols - 1 ) - colNames += tr( "and " ); - colNames += quotedValue( QString::fromUtf8( PQgetvalue( types, j, 0 ) ) ); - if ( j < numCols - 2 ) - colNames += ","; - } - - log.append( tr( "The unique index based on columns %1 is unsuitable because Quantum GIS does not currently " - "support multiple columns as a key into the table.\n" ).arg( colNames ) ); - } - } - - // suitableKeyColumns now contains the name of columns (and their - // data type) that - // are suitable for use as a key into the table. If there is - // more than one we need to choose one. For the moment, just - // choose the first in the list. - - if ( suitableKeyColumns.size() > 0 ) - { - primaryKey = suitableKeyColumns[0].first; - primaryKeyType = suitableKeyColumns[0].second; - } - else - { - // If there is an oid on the table, use that instead, - // otherwise give up - sql = QString( "select attname from pg_attribute where attname='oid' and attrelid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); - Result oidCheck = connectionRO->PQexec( sql ); - - if ( PQntuples( oidCheck ) != 0 ) - { - primaryKey = "oid"; - primaryKeyType = "oid"; - } - else - { - log.prepend( "There were no columns in the table that were suitable " - "as a qgis key into the table (either a column with a " - "unique index and type integer or a PostgreSQL oid column.\n" ); - } - } - - // Either primaryKey has been set by the above code, or it - // hasn't. If not, present some info to the user to give them some - // idea of why not. - if ( primaryKey.isEmpty() ) - { - // Give some info to the user about why things didn't work out. - valid = false; - showMessageBox( tr( "Unable to find a key column" ), log ); - } - else - { - mPrimaryKeyDefault = defaultValue( primaryKey ).toString(); - if ( mPrimaryKeyDefault.isNull() ) - { - mPrimaryKeyDefault = QString( "max(%1)+1 from %2.%3" ) - .arg( quotedIdentifier( primaryKey ) ) - .arg( quotedIdentifier( mSchemaName ) ) - .arg( quotedIdentifier( mTableName ) ); - } - } - } - } - - if ( !primaryKey.isNull() ) - { - QgsDebugMsg( "row key is " + primaryKey ); - } - else - { - QgsDebugMsg( "row key was not set." ); - } - - return primaryKey; -} - -void QgsPostgresProvider::parseView() -{ - // Have a poke around the view to see if any of the columns - // could be used as the primary key. - tableCols cols; - - // Given a schema.view, populate the cols variable with the - // schema.table.column's that underly the view columns. - findColumns( cols ); - - // pick the primary key, if we don't have one yet - if ( primaryKey.isEmpty() ) - { - // From the view columns, choose one for which the underlying - // column is suitable for use as a key into the view. - primaryKey = chooseViewColumn( cols ); - } - - tableCols::const_iterator it = cols.find( primaryKey ); - if ( it != cols.end() ) - { - mPrimaryKeyDefault = defaultValue( it->second.column, it->second.relation, it->second.schema ).toString(); - if ( mPrimaryKeyDefault.isNull() ) - { - mPrimaryKeyDefault = QString( "max(%1)+1 from %2.%3" ) - .arg( quotedIdentifier( it->second.column ) ) - .arg( quotedIdentifier( it->second.schema ) ) - .arg( quotedIdentifier( it->second.relation ) ); - } - } - else - { - mPrimaryKeyDefault = QString( "max(%1)+1 from %2.%3" ) - .arg( quotedIdentifier( primaryKey ) ) - .arg( quotedIdentifier( mSchemaName ) ) - .arg( quotedIdentifier( mTableName ) ); - } -} - -QString QgsPostgresProvider::primaryKeyDefault() -{ - if ( mPrimaryKeyDefault.isNull() ) - parseView(); - - return mPrimaryKeyDefault; -} - -// Given the table and column that each column in the view refers to, -// choose one. Prefers column with an index on them, but will -// otherwise choose something suitable. - -QString QgsPostgresProvider::chooseViewColumn( const tableCols &cols ) -{ - // For each relation name and column name need to see if it - // has unique constraints on it, or is a primary key (if not, - // it shouldn't be used). Should then be left with one or more - // entries in the map which can be used as the key. - - QString sql, key; - QStringList log; - tableCols suitable; - // Cache of relation oid's - std::map relOid; - - std::vector oids; - tableCols::const_iterator iter = cols.begin(); - for ( ; iter != cols.end(); ++iter ) - { - QString viewCol = iter->first; - QString schemaName = iter->second.schema; - QString tableName = iter->second.relation; - QString tableCol = iter->second.column; - QString colType = iter->second.type; - - // Get the oid from pg_class for the given schema.relation for use - // in subsequent queries. - sql = QString( "select regclass(%1)::oid" ).arg( quotedValue( quotedIdentifier( schemaName ) + "." + quotedIdentifier( tableName ) ) ); - Result result = connectionRO->PQexec( sql ); - QString rel_oid; - if ( PQntuples( result ) == 1 ) - { - rel_oid = PQgetvalue( result, 0, 0 ); - // Keep the rel_oid for use later one. - relOid[viewCol] = rel_oid; - } - else - { - QgsMessageLog::logMessage( tr( "Relation %1.%2 doesn't exist in the pg_class table. This shouldn't happen and is odd." ) - .arg( schemaName ).arg( tableName ) ); - continue; - } - - // This sql returns one or more rows if the column 'tableCol' in - // table 'tableName' and schema 'schemaName' has one or more - // columns that satisfy the following conditions: - // 1) the column has data type of integer. - // 2) the column has a unique constraint or primary key constraint - // on it. - // 3) the constraint applies just to the column of interest (i.e., - // it isn't a constraint over multiple columns. - sql = QString( "select * from pg_constraint where " - "conkey[1]=(select attnum from pg_attribute where attname=%1 and attrelid=%2) " - "and conrelid=%2 and (contype='p' or contype='u') " - "and array_dims(conkey)='[1:1]'" ).arg( quotedValue( tableCol ) ).arg( rel_oid ); - - result = connectionRO->PQexec( sql ); - if ( PQntuples( result ) == 1 && - ( colType == "int2" || colType == "int4" || colType == "int8" ) ) - suitable[viewCol] = iter->second; - - QString details = tr( "'%1' derives from '%2.%3.%4' " ).arg( viewCol ).arg( schemaName ).arg( tableName ).arg( tableCol ); - - if ( PQntuples( result ) == 1 && - ( colType == "int2" || colType == "int4" || colType == "int8" ) ) - { - details += tr( "and is suitable." ); - } - else - { - details += tr( "and is not suitable (type is %1)" ).arg( colType ); - if ( PQntuples( result ) == 1 ) - details += tr( " and has a suitable constraint)" ); - else - details += tr( " and does not have a suitable constraint)" ); - } - - log << details; - - if ( tableCol == "oid" ) - oids.push_back( iter ); - } - - // 'oid' columns in tables don't have a constraint on them, but - // they are useful to consider, so add them in if not already - // here. - for ( uint i = 0; i < oids.size(); ++i ) - { - if ( suitable.find( oids[i]->first ) == suitable.end() ) - { - suitable[oids[i]->first] = oids[i]->second; - - QgsDebugMsg( "Adding column " + oids[i]->first + " as it may be suitable." ); - } - } - - // Now have a map containing all of the columns in the view that - // might be suitable for use as the key to the table. Need to choose - // one thus: - // - // If there is more than one suitable column pick one that is - // indexed, else pick one called 'oid' if it exists, else - // pick the first one. If there are none we return an empty string. - - // Search for one with an index - tableCols::const_iterator i = suitable.begin(); - for ( ; i != suitable.end(); ++i ) - { - // Get the relation oid from our cache. - QString rel_oid = relOid[i->first]; - // And see if the column has an index - sql = QString( "select * from pg_index where indrelid=%1 and indkey[0]=(select attnum from pg_attribute where attrelid=%1 and attname=%2)" ) - .arg( rel_oid ) - .arg( quotedValue( i->second.column ) ); - Result result = connectionRO->PQexec( sql ); - - if ( PQntuples( result ) > 0 && uniqueData( mQuery, i->first ) ) - { - // Got one. Use it. - key = i->first; - QgsDebugMsg( "Picked column '" + key + "' because it has an index." ); - break; - } - } - - if ( key.isEmpty() ) - { - // If none have indices, choose one that is called 'oid' (if it - // exists). This is legacy support and could be removed in - // future. - i = suitable.find( "oid" ); - if ( i != suitable.end() && uniqueData( mQuery, i->first ) ) - { - key = i->first; - - QgsDebugMsg( "Picked column " + key + - " as it is probably the postgresql object id " - " column (which contains unique values) and there are no" - " columns with indices to choose from." ); - } - // else choose the first one in the container that has unique data - else - { - tableCols::const_iterator i = suitable.begin(); - for ( ; i != suitable.end(); ++i ) - { - if ( uniqueData( mQuery, i->first ) ) - { - key = i->first; - - QgsDebugMsg( "Picked column " + key + - " as it was the first suitable column found" - " with unique data and were are no" - " columns with indices to choose from" ); - break; - } - else - { - log << tr( "Note: '%1' initially appeared suitable" - " but does not contain unique data, so is not suitable.\n" ) - .arg( i->first ); - } - } - } - } - - if ( key.isEmpty() ) - { - valid = false; - log.prepend( tr( "The view '%1.%2' has no column suitable for use as a unique key.\n" - "Quantum GIS requires that the view has a column that can be used " - "as a unique key. Such a column should be derived from " - "a table column of type integer and be a primary key, " - "have a unique constraint on it, or be a PostgreSQL " - "oid column. To improve performance the column should also be indexed.\n" - "The view you selected has the following columns, none " - "of which satisfy the above conditions:" ).arg( mSchemaName ).arg( mTableName ) ); - showMessageBox( tr( "No suitable key column in view" ), log ); - } - - return key; -} - -bool QgsPostgresProvider::uniqueData( QString query, QString colName ) -{ - Q_UNUSED( query ); - // Check to see if the given column contains unique data - QString sql = QString( "select count(distinct %1)=count(%1) from %2" ) - .arg( quotedIdentifier( colName ) ) - .arg( mQuery ); - - if ( !sqlWhereClause.isEmpty() ) - { - sql += " where " + sqlWhereClause; - } - - Result unique = connectionRO->PQexec( sql ); - - if ( PQresultStatus( unique ) != PGRES_TUPLES_OK ) - { - pushError( QString::fromUtf8( PQresultErrorMessage( unique ) ) ); - return false; - } - - return PQntuples( unique ) == 1 - && QString::fromUtf8( PQgetvalue( unique, 0, 0 ) ).startsWith( "t" ); -} - -int QgsPostgresProvider::SRCFromViewColumn( const QString& ns, const QString& relname, const QString& attname_table, const QString& attname_view, const QString& viewDefinition, SRC& result ) const -{ - Q_UNUSED( attname_table ); - Q_UNUSED( attname_view ); - Q_UNUSED( viewDefinition ); - QString newViewDefSql = QString( "SELECT definition FROM pg_views WHERE schemaname=%1 AND viewname=%2" ) - .arg( quotedValue( ns ) ).arg( quotedValue( relname ) ); - Result newViewDefResult = connectionRO->PQexec( newViewDefSql ); - int numEntries = PQntuples( newViewDefResult ); - - if ( numEntries > 0 ) //relation is a view - { - QString newViewDefinition( QString::fromUtf8( PQgetvalue( newViewDefResult, 0, 0 ) ) ); - - QString newAttNameView = attname_table; - QString newAttNameTable = attname_table; - - //find out the attribute name of the underlying table/view - if ( newViewDefinition.contains( " AS " ) ) - { - QRegExp s( "(\\w+)" + QString( " AS " ) + QRegExp::escape( attname_table ) ); - if ( s.indexIn( newViewDefinition ) != -1 ) - { - newAttNameTable = s.cap( 1 ); - } - } - - QString viewColumnSql = - QString( "SELECT " - "table_schema," - "table_name," - "column_name" - " FROM " - "(" - "SELECT DISTINCT " - "current_database()::information_schema.sql_identifier AS view_catalog," - "nv.nspname::information_schema.sql_identifier AS view_schema," - "v.relname::information_schema.sql_identifier AS view_name," - "current_database()::information_schema.sql_identifier AS table_catalog," - "nt.nspname::information_schema.sql_identifier AS table_schema," - "t.relname::information_schema.sql_identifier AS table_name," - "a.attname::information_schema.sql_identifier AS column_name" - " FROM " - "pg_namespace nv," - "pg_class v," - "pg_depend dv," - "pg_depend dt," - "pg_class t," - "pg_namespace nt," - "pg_attribute a" - " WHERE " - "nv.oid=v.relnamespace AND " - "v.relkind='v'::\"char\" AND " - "v.oid=dv.refobjid AND " - "dv.refclassid='pg_class'::regclass::oid AND " - "dv.classid='pg_rewrite'::regclass::oid AND " - "dv.deptype='i'::\"char\" AND " - "dv.objid = dt.objid AND " - "dv.refobjid<>dt.refobjid AND " - "dt.classid='pg_rewrite'::regclass::oid AND " - "dt.refclassid='pg_class'::regclass::oid AND " - "dt.refobjid=t.oid AND " - "t.relnamespace = nt.oid AND " - "(t.relkind=ANY (ARRAY['r'::\"char\", 'v'::\"char\"])) AND " - "t.oid=a.attrelid AND " - "dt.refobjsubid=a.attnum" - " ORDER BY " - "current_database()::information_schema.sql_identifier," - "nv.nspname::information_schema.sql_identifier," - "v.relname::information_schema.sql_identifier," - "current_database()::information_schema.sql_identifier," - "nt.nspname::information_schema.sql_identifier," - "t.relname::information_schema.sql_identifier," - "a.attname::information_schema.sql_identifier" - ") x" - " WHERE " - "view_schema=%1 AND " - "view_name=%2 AND " - "column_name=%3" ) - .arg( quotedValue( ns ) ) - .arg( quotedValue( relname ) ) - .arg( quotedValue( newAttNameTable ) ); - - Result viewColumnResult = connectionRO->PQexec( viewColumnSql ); - if ( PQntuples( viewColumnResult ) > 0 ) - { - QString newTableSchema = QString::fromUtf8( PQgetvalue( viewColumnResult, 0, 0 ) ); - QString newTableName = QString::fromUtf8( PQgetvalue( viewColumnResult, 0, 1 ) ); - int retvalue = SRCFromViewColumn( newTableSchema, newTableName, newAttNameTable, newAttNameView, newViewDefinition, result ); - return retvalue; - } - else - { - return 1; - } - - } - - //relation is table, we just have to add the type - QString typeSql = QString( "SELECT " - "pg_type.typname" - " FROM " - "pg_attribute," - "pg_class," - "pg_namespace," - "pg_type" - " WHERE " - "pg_class.relname=%1 AND " - "pg_namespace.nspname=%2 AND " - "pg_attribute.attname=%3 AND " - "pg_attribute.attrelid=pg_class.oid AND " - "pg_class.relnamespace=pg_namespace.oid AND " - "pg_attribute.atttypid=pg_type.oid" ) - .arg( quotedValue( relname ) ) - .arg( quotedValue( ns ) ) - .arg( quotedValue( attname_table ) ); - - Result typeSqlResult = connectionRO->PQexec( typeSql ); - if ( PQntuples( typeSqlResult ) < 1 ) - { - return 1; - } - QString type = QString::fromUtf8( PQgetvalue( typeSqlResult, 0, 0 ) ); - - result.schema = ns; - result.relation = relname; - result.column = attname_table; - result.type = type; - return 0; -} - -// This function will return in the cols variable the -// underlying view and columns for each column in -// mSchemaName.mTableName. - -void QgsPostgresProvider::findColumns( tableCols& cols ) -{ - QString viewColumnSql = - QString( "SELECT " - "table_schema," - "table_name," - "column_name" - " FROM " - "(" - "SELECT DISTINCT " - "current_database() AS view_catalog," - "nv.nspname AS view_schema," - "v.relname AS view_name," - "current_database() AS table_catalog," - "nt.nspname AS table_schema," - "t.relname AS table_name," - "a.attname AS column_name" - " FROM " - "pg_namespace nv," - "pg_class v," - "pg_depend dv," - "pg_depend dt," - "pg_class t," - "pg_namespace nt," - "pg_attribute a" - " WHERE " - "nv.oid=v.relnamespace AND " - "v.relkind='v'::\"char\" AND " - "v.oid=dv.refobjid AND " - "dv.refclassid='pg_class'::regclass::oid AND " - "dv.classid='pg_rewrite'::regclass::oid AND " - "dv.deptype='i'::\"char\" AND " - "dv.objid=dt.objid AND " - "dv.refobjid<>dt.refobjid AND " - "dt.classid='pg_rewrite'::regclass::oid AND " - "dt.refclassid='pg_class'::regclass::oid AND " - "dt.refobjid=t.oid AND " - "t.relnamespace=nt.oid AND " - "(t.relkind = ANY (ARRAY['r'::\"char\",'v'::\"char\"])) AND " - "t.oid=a.attrelid AND " - "dt.refobjsubid=a.attnum" - " ORDER BY " - "current_database()," - "nv.nspname," - "v.relname," - "current_database()," - "nt.nspname," - "t.relname," - "a.attname" - ") x" - " WHERE " - "view_schema=%1 AND view_name=%2" ) - .arg( quotedValue( mSchemaName ) ) - .arg( quotedValue( mTableName ) ); - - if ( !primaryKey.isEmpty() ) - { - viewColumnSql += QString( " AND column_name=%1" ).arg( quotedValue( primaryKey ) ); - } - - Result viewColumnResult = connectionRO->PQexec( viewColumnSql ); - - //find out view definition - QString viewDefSql = QString( "SELECT definition FROM pg_views WHERE schemaname=%1 AND viewname=%2" ) - .arg( quotedValue( mSchemaName ) ) - .arg( quotedValue( mTableName ) ); - Result viewDefResult = connectionRO->PQexec( viewDefSql ); - if ( PQntuples( viewDefResult ) < 1 ) - { - return; - } - - QString viewDefinition( QString::fromUtf8( PQgetvalue( viewDefResult, 0, 0 ) ) ); - - QString ns, relname, attname_table, attname_view; - SRC columnInformation; - - for ( int i = 0; i < PQntuples( viewColumnResult ); ++i ) - { - ns = QString::fromUtf8( PQgetvalue( viewColumnResult, i, 0 ) ); - relname = QString::fromUtf8( PQgetvalue( viewColumnResult, i, 1 ) ); - attname_table = QString::fromUtf8( PQgetvalue( viewColumnResult, i, 2 ) ); - - //find out original attribute name - attname_view = attname_table; - - //examine if the column name has been renamed in the view with AS - if ( viewDefinition.contains( " AS " ) ) - { - // This regular expression needs more testing. Since the view - // definition comes from postgresql and has been 'standardised', we - // don't need to deal with everything that the user could put in a view - // definition. Does the regexp have to deal with the schema?? - - QRegExp s( ".* \"?" + QRegExp::escape( relname ) + - "\"?\\.\"?" + QRegExp::escape( attname_table ) + - "\"? AS \"?(\\w+)\"?,* .*" ); - - QgsDebugMsg( viewDefinition + "\n" + s.pattern() ); - - if ( s.indexIn( viewDefinition ) != -1 ) - { - attname_view = s.cap( 1 ); - QgsDebugMsg( QString( "original view column name was: %1" ).arg( attname_view ) ); - } - } - - SRCFromViewColumn( ns, relname, attname_table, attname_view, viewDefinition, columnInformation ); - cols.insert( std::make_pair( attname_view, columnInformation ) ); - QgsDebugMsg( "Inserting into cols (for key " + attname_view + " ): " + columnInformation.schema + "." + columnInformation.relation + "." + columnInformation.column + "." + columnInformation.type ); - } -} - -// Returns the minimum value of an attribute -QVariant QgsPostgresProvider::minimumValue( int index ) -{ - try - { - // get the field name - const QgsField &fld = field( index ); - QString sql = QString( "select min(%1) from %2" ) - .arg( quotedIdentifier( fld.name() ) ) - .arg( mQuery ); - - if ( !sqlWhereClause.isEmpty() ) - { - sql += QString( " where %1" ).arg( sqlWhereClause ); - } - - Result rmin = connectionRO->PQexec( sql ); - return convertValue( fld.type(), QString::fromUtf8( PQgetvalue( rmin, 0, 0 ) ) ); - } - catch ( PGFieldNotFound ) - { - return QVariant( QString::null ); - } -} - -// Returns the list of unique values of an attribute -void QgsPostgresProvider::uniqueValues( int index, QList &uniqueValues, int limit ) -{ - uniqueValues.clear(); - - try - { - // get the field name - const QgsField &fld = field( index ); - QString sql = QString( "select distinct %1 from %2" ) - .arg( quotedIdentifier( fld.name() ) ) - .arg( mQuery ); - - if ( !sqlWhereClause.isEmpty() ) - { - sql += QString( " where %1" ).arg( sqlWhereClause ); - } - - sql += QString( " order by %1" ) - .arg( quotedIdentifier( fld.name() ) ); - - if ( limit >= 0 ) - { - sql += QString( " LIMIT %1" ).arg( limit ); - } - - Result res = connectionRO->PQexec( sql ); - if ( PQresultStatus( res ) == PGRES_TUPLES_OK ) - { - for ( int i = 0; i < PQntuples( res ); i++ ) - uniqueValues.append( convertValue( fld.type(), QString::fromUtf8( PQgetvalue( res, i, 0 ) ) ) ); - } - } - catch ( PGFieldNotFound ) - { - } -} - -void QgsPostgresProvider::enumValues( int index, QStringList& enumList ) -{ - enumList.clear(); - - QString typeName; - //find out type of index - QgsFieldMap::const_iterator f_it = attributeFields.find( index ); - if ( f_it != attributeFields.constEnd() ) - { - typeName = f_it.value().typeName(); - } - else - { - return; - } - - //is type an enum? - QString typeSql = QString( "SELECT typtype FROM pg_type where typname = %1" ).arg( quotedValue( typeName ) ); - Result typeRes = connectionRO->PQexec( typeSql ); - if ( PQresultStatus( typeRes ) != PGRES_TUPLES_OK || PQntuples( typeRes ) < 1 ) - { - return; - } - - - QString typtype = PQgetvalue( typeRes, 0, 0 ); - if ( typtype.compare( "e", Qt::CaseInsensitive ) == 0 ) - { - //try to read enum_range of attribute - if ( !parseEnumRange( enumList, f_it->name() ) ) - { - enumList.clear(); - } - } - else - { - //is there a domain check constraint for the attribute? - if ( !parseDomainCheckConstraint( enumList, f_it->name() ) ) - { - enumList.clear(); - } - } -} - -bool QgsPostgresProvider::parseEnumRange( QStringList& enumValues, const QString& attributeName ) const -{ - enumValues.clear(); - - QString enumRangeSql = QString( "SELECT enumlabel FROM pg_catalog.pg_enum WHERE enumtypid=(SELECT atttypid::regclass FROM pg_attribute WHERE attrelid=%1::regclass AND attname=%2)" ) - .arg( quotedValue( mQuery ) ) - .arg( quotedValue( attributeName ) ); - Result enumRangeRes = connectionRO->PQexec( enumRangeSql ); - - if ( PQresultStatus( enumRangeRes ) != PGRES_TUPLES_OK ) - return false; - - for ( int i = 0; i < PQntuples( enumRangeRes ); i++ ) - { - enumValues << QString::fromUtf8( PQgetvalue( enumRangeRes, i, 0 ) ); - } - - return true; -} - -bool QgsPostgresProvider::parseDomainCheckConstraint( QStringList& enumValues, const QString& attributeName ) const -{ - enumValues.clear(); - - //is it a domain type with a check constraint? - QString domainSql = QString( "SELECT domain_name from information_schema.columns where table_name=%1 and column_name=%2" ).arg( quotedValue( mTableName ) ).arg( quotedValue( attributeName ) ); - Result domainResult = connectionRO->PQexec( domainSql ); - if ( PQresultStatus( domainResult ) == PGRES_TUPLES_OK && PQntuples( domainResult ) > 0 ) - { - //a domain type - QString domainCheckDefinitionSql = QString( "SELECT consrc FROM pg_constraint where conname=(SELECT constraint_name FROM information_schema.domain_constraints WHERE domain_name=%1)" ).arg( quotedValue( PQgetvalue( domainResult, 0, 0 ) ) ); - Result domainCheckRes = connectionRO->PQexec( domainCheckDefinitionSql ); - if ( PQresultStatus( domainCheckRes ) == PGRES_TUPLES_OK && PQntuples( domainCheckRes ) > 0 ) - { - QString checkDefinition = QString::fromUtf8( PQgetvalue( domainCheckRes, 0, 0 ) ); - - //we assume that the constraint is of the following form: - //(VALUE = ANY (ARRAY['a'::text, 'b'::text, 'c'::text, 'd'::text])) - //normally, postgresql creates that if the contstraint has been specified as 'VALUE in ('a', 'b', 'c', 'd') - - int anyPos = checkDefinition.indexOf( QRegExp( "VALUE\\s*=\\s*ANY\\s*\\(\\s*ARRAY\\s*\\[" ) ); - int arrayPosition = checkDefinition.lastIndexOf( "ARRAY[" ); - int closingBracketPos = checkDefinition.indexOf( "]", arrayPosition + 6 ); - - if ( anyPos == -1 || anyPos >= arrayPosition ) - { - return false; //constraint has not the required format - } - - if ( arrayPosition != -1 ) - { - QString valueList = checkDefinition.mid( arrayPosition + 6, closingBracketPos ); - QStringList commaSeparation = valueList.split( ",", QString::SkipEmptyParts ); - QStringList::const_iterator cIt = commaSeparation.constBegin(); - for ( ; cIt != commaSeparation.constEnd(); ++cIt ) - { - //get string between '' - int beginQuotePos = cIt->indexOf( "'" ); - int endQuotePos = cIt->lastIndexOf( "'" ); - if ( beginQuotePos != -1 && ( endQuotePos - beginQuotePos ) > 1 ) - { - enumValues << cIt->mid( beginQuotePos + 1, endQuotePos - beginQuotePos - 1 ); - } - } - } - return true; - } - } - return false; -} - -// Returns the maximum value of an attribute -QVariant QgsPostgresProvider::maximumValue( int index ) -{ - try - { - // get the field name - const QgsField &fld = field( index ); - QString sql = QString( "select max(%1) from %2" ) - .arg( quotedIdentifier( fld.name() ) ) - .arg( mQuery ); - - if ( !sqlWhereClause.isEmpty() ) - { - sql += QString( " where %1" ).arg( sqlWhereClause ); - } - - Result rmax = connectionRO->PQexec( sql ); - return convertValue( fld.type(), QString::fromUtf8( PQgetvalue( rmax, 0, 0 ) ) ); - } - catch ( PGFieldNotFound ) - { - return QVariant( QString::null ); - } -} - - -bool QgsPostgresProvider::isValid() -{ - return valid; -} - -QVariant QgsPostgresProvider::defaultValue( QString fieldName, QString tableName, QString schemaName ) -{ - if ( schemaName.isNull() ) - schemaName = mSchemaName; - if ( tableName.isNull() ) - tableName = mTableName; - - // Get the default column value from the Postgres information - // schema. If there is no default we return an empty string. - - // Maintaining a cache of the results of this query would be quite - // simple and if this query is called lots, could save some time. - - QString sql( "SELECT column_default FROM" - " information_schema.columns WHERE" - " column_default IS NOT NULL" - " AND table_schema = " + quotedValue( schemaName ) + - " AND table_name = " + quotedValue( tableName ) + - " AND column_name = " + quotedValue( fieldName ) ); - - QVariant defaultValue( QString::null ); - - Result result = connectionRO->PQexec( sql ); - - if ( PQntuples( result ) == 1 && !PQgetisnull( result, 0, 0 ) ) - defaultValue = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - - return defaultValue; -} - -QVariant QgsPostgresProvider::defaultValue( int fieldId ) -{ - try - { - return defaultValue( field( fieldId ).name() ); - } - catch ( PGFieldNotFound ) - { - return QVariant( QString::null ); - } -} - -/** - * Check to see if GEOS is available - */ -bool QgsPostgresProvider::Conn::hasGEOS() -{ - // make sure info is up to date for the current connection - postgisVersion(); - // get geos capability - return geosAvailable; -} - -/** - * Check to see if topology is available - */ -bool QgsPostgresProvider::Conn::hasTopology() -{ - // make sure info is up to date for the current connection - postgisVersion(); - // get topology capability - return topologyAvailable; -} - -/* Functions for determining available features in postGIS */ -QString QgsPostgresProvider::Conn::postgisVersion() -{ - if ( gotPostgisVersion ) return postgisVersionInfo; - - postgresqlVersion = PQserverVersion( conn ); - - Result result = PQexec( "select postgis_version()" ); - if ( PQntuples( result ) != 1 ) - { - QgsMessageLog::logMessage( tr( "Retrieval of postgis version failed" ), tr( "PostgreSQL" ) ); - return QString::null; - } - - postgisVersionInfo = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - - QgsDebugMsg( "PostGIS version info: " + postgisVersionInfo ); - - QStringList postgisParts = postgisVersionInfo.split( " ", QString::SkipEmptyParts ); - - // Get major and minor version - QStringList postgisVersionParts = postgisParts[0].split( ".", QString::SkipEmptyParts ); - if ( postgisVersionParts.size() < 2 ) - { - QgsMessageLog::logMessage( tr( "Could not parse postgis version string '%1'" ).arg( postgisVersionInfo ), tr( "PostgreSQL" ) ); - return QString::null; - } - - postgisVersionMajor = postgisVersionParts[0].toInt(); - postgisVersionMinor = postgisVersionParts[1].toInt(); - - mUseWkbHex = postgisVersionMajor < 1; - - // apparently postgis 1.5.2 doesn't report capabilities in postgis_version() anymore - if ( postgisVersionMajor > 1 || ( postgisVersionMajor == 1 && postgisVersionMinor >= 5 ) ) - { - result = PQexec( "select postgis_geos_version(),postgis_proj_version()" ); - geosAvailable = PQntuples( result ) == 1 && !PQgetisnull( result, 0, 0 ); - projAvailable = PQntuples( result ) == 1 && !PQgetisnull( result, 0, 1 ); - QgsDebugMsg( QString( "geos:%1 proj:%2" ) - .arg( geosAvailable ? PQgetvalue( result, 0, 0 ) : "none" ) - .arg( projAvailable ? PQgetvalue( result, 0, 1 ) : "none" ) ); - gistAvailable = true; - } - else - { - // assume no capabilities - geosAvailable = false; - gistAvailable = false; - projAvailable = false; - - // parse out the capabilities and store them - QStringList geos = postgisParts.filter( "GEOS" ); - if ( geos.size() == 1 ) - { - geosAvailable = ( geos[0].indexOf( "=1" ) > -1 ); - } - QStringList gist = postgisParts.filter( "STATS" ); - if ( gist.size() == 1 ) - { - gistAvailable = ( geos[0].indexOf( "=1" ) > -1 ); - } - QStringList proj = postgisParts.filter( "PROJ" ); - if ( proj.size() == 1 ) - { - projAvailable = ( proj[0].indexOf( "=1" ) > -1 ); - } - } - - // checking for topology support - QgsDebugMsg( "Checking for topology support" ); - topologyAvailable = false; - if ( postgisVersionMajor > 1 ) - { - Result result = PQexec( "select count(c.oid) from pg_class as c join pg_namespace as n on c.relnamespace = n.oid where n.nspname = 'topology' and c.relname = 'topology'" ); - if ( PQntuples( result ) >= 1 ) - { - topologyAvailable = true; - } - } - - gotPostgisVersion = true; - - return postgisVersionInfo; -} - -QString QgsPostgresProvider::paramValue( QString fieldValue, const QString &defaultValue ) const -{ - if ( fieldValue.isNull() ) - return QString::null; - - if ( fieldValue == defaultValue && !defaultValue.isNull() ) - { - PGresult *result = connectionRW->PQexec( QString( "select %1" ).arg( defaultValue ) ); - if ( PQresultStatus( result ) == PGRES_FATAL_ERROR ) - throw PGException( result ); - - if ( PQgetisnull( result, 0, 0 ) ) - { - PQclear( result ); - return QString::null; - } - else - { - QString val = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - PQclear( result ); - return val; - } - } - - return fieldValue; -} - -bool QgsPostgresProvider::addFeatures( QgsFeatureList &flist ) -{ - if ( flist.size() == 0 ) - return true; - - if ( isQuery ) - return false; - - if ( !connectRW() ) - return false; - - bool returnvalue = true; - - try - { - connectionRW->PQexecNR( "BEGIN" ); - - // Prepare the INSERT statement - QString insert = QString( "INSERT INTO %1 (" ).arg( mQuery ); - QString values = ") VALUES ("; - QString delim = ","; - int offset = 1; - - if ( !geometryColumn.isNull() ) - { - insert += quotedIdentifier( geometryColumn ); - values += QString( "%1($%2%3,%4)" ) - .arg( connectionRO->majorVersion() < 2 ? "geomfromwkb" : "st_geomfromwkb" ) - .arg( offset ) - .arg( connectionRW->useWkbHex() ? "" : "::bytea" ) - .arg( srid ); - offset += 1; - delim = ","; - } - else - { - delim = ""; - } - - if ( primaryKeyType != "tid" && primaryKeyType != "oid" ) - { - insert += delim + quotedIdentifier( primaryKey ); - values += delim + QString( "$%1" ).arg( offset ); - offset += 1; - delim = ","; - } - - const QgsAttributeMap &attributevec = flist[0].attributeMap(); - - QStringList defaultValues; - QList fieldId; - - // look for unique attribute values to place in statement instead of passing as parameter - // e.g. for defaults - for ( QgsAttributeMap::const_iterator it = attributevec.begin(); it != attributevec.end(); it++ ) - { - QgsFieldMap::const_iterator fit = attributeFields.find( it.key() ); - if ( fit == attributeFields.end() ) - continue; - - QString fieldname = fit->name(); - - QgsDebugMsg( "Checking field against: " + fieldname ); - - if ( fieldname.isEmpty() || fieldname == geometryColumn || fieldname == primaryKey ) - continue; - - int i; - for ( i = 1; i < flist.size(); i++ ) - { - const QgsAttributeMap &attributevec = flist[i].attributeMap(); - - QgsAttributeMap::const_iterator thisit = attributevec.find( it.key() ); - if ( thisit == attributevec.end() ) - break; - - if ( *thisit != *it ) - break; - } - - insert += delim + quotedIdentifier( fieldname ); - - QString defVal = defaultValue( it.key() ).toString(); - - if ( i == flist.size() ) - { - if ( *it == defVal ) - { - if ( defVal.isNull() ) - { - values += delim + "NULL"; - } - else - { - values += delim + defVal; - } - } - else if ( fit->typeName() == "geometry" ) - { - values += QString( "%1%2(%3)" ) - .arg( delim ) - .arg( connectionRO->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" ) - .arg( quotedValue( it->toString() ) ); - } - else if ( fit->typeName() == "geography" ) - { - values += QString( "%1st_geographyfromewkt(%2)" ) - .arg( delim ) - .arg( quotedValue( it->toString() ) ); - } - else - { - values += delim + quotedValue( it->toString() ); - } - } - else - { - // value is not unique => add parameter - if ( fit->typeName() == "geometry" ) - { - values += QString( "%1%2($%3)" ) - .arg( delim ) - .arg( connectionRO->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" ) - .arg( defaultValues.size() + offset ); - } - else if ( fit->typeName() == "geography" ) - { - values += QString( "%1st_geographyfromewkt($%2)" ) - .arg( delim ) - .arg( defaultValues.size() + offset ); - } - else - { - values += QString( "%1$%2" ) - .arg( delim ) - .arg( defaultValues.size() + offset ); - } - defaultValues.append( defVal ); - fieldId.append( it.key() ); - } - - delim = ","; - } - - insert += values + ")"; - - QgsDebugMsg( QString( "prepare addfeatures: %1" ).arg( insert ) ); - PGresult *stmt = connectionRW->PQprepare( "addfeatures", insert, fieldId.size() + offset - 1, NULL ); - if ( stmt == 0 || PQresultStatus( stmt ) == PGRES_FATAL_ERROR ) - throw PGException( stmt ); - PQclear( stmt ); - - QList newIds; - - for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); features++ ) - { - const QgsAttributeMap &attributevec = features->attributeMap(); - - QStringList params; - if ( !geometryColumn.isNull() ) - { - QString geomParam; - appendGeomString( features->geometry(), geomParam ); - - params << geomParam; - } - - if ( primaryKeyType != "tid" && primaryKeyType != "oid" ) - { - int id = paramValue( primaryKeyDefault(), primaryKeyDefault() ).toInt(); - params << QString::number( id ); - newIds << id; - } - - for ( int i = 0; i < fieldId.size(); i++ ) - params << paramValue( attributevec[ fieldId[i] ].toString(), defaultValues[i] ); - - PGresult *result = connectionRW->PQexecPrepared( "addfeatures", params ); - if ( result == 0 || PQresultStatus( result ) == PGRES_FATAL_ERROR ) - throw PGException( result ); - PQclear( result ); - } - - if ( flist.size() == newIds.size() ) - for ( int i = 0; i < flist.size(); i++ ) - flist[i].setFeatureId( newIds[i] ); - - connectionRW->PQexecNR( "DEALLOCATE addfeatures" ); - connectionRW->PQexecNR( "COMMIT" ); - - featuresCounted += flist.size(); - } - catch ( PGException &e ) - { - e.showErrorMessage( tr( "Error while adding features" ) ); - connectionRW->PQexecNR( "ROLLBACK" ); - connectionRW->PQexecNR( "DEALLOCATE addfeatures" ); - returnvalue = false; - } - - rewind(); - return returnvalue; -} - -bool QgsPostgresProvider::deleteFeatures( const QgsFeatureIds & id ) -{ - bool returnvalue = true; - - if ( isQuery ) - return false; - - if ( !connectRW() ) - return false; - - try - { - connectionRW->PQexecNR( "BEGIN" ); - - for ( QgsFeatureIds::const_iterator it = id.begin(); it != id.end(); ++it ) - { - QString sql = QString( "DELETE FROM %1 WHERE %2" ) - .arg( mQuery ).arg( whereClause( *it ) ); - QgsDebugMsg( "delete sql: " + sql ); - - //send DELETE statement and do error handling - PGresult *result = connectionRW->PQexec( sql ); - if ( result == 0 || PQresultStatus( result ) == PGRES_FATAL_ERROR ) - throw PGException( result ); - PQclear( result ); - } - - connectionRW->PQexecNR( "COMMIT" ); - - featuresCounted -= id.size(); - } - catch ( PGException &e ) - { - e.showErrorMessage( tr( "Error while deleting features" ) ); - connectionRW->PQexecNR( "ROLLBACK" ); - returnvalue = false; - } - rewind(); - return returnvalue; -} - -bool QgsPostgresProvider::addAttributes( const QList &attributes ) -{ - bool returnvalue = true; - - if ( isQuery ) - return false; - - if ( !connectRW() ) - return false; - - try - { - connectionRW->PQexecNR( "BEGIN" ); - - for ( QList::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter ) - { - QString type = iter->typeName(); - if ( type == "char" || type == "varchar" ) - { - if ( iter->length() > 0 ) - type = QString( "%1(%2)" ).arg( type ).arg( iter->length() ); - } - else if ( type == "numeric" || type == "decimal" ) - { - if ( iter->length() > 0 && iter->precision() > 0 ) - type = QString( "%1(%2,%3)" ).arg( type ).arg( iter->length() ).arg( iter->precision() ); - } - - QString sql = QString( "ALTER TABLE %1 ADD COLUMN %2 %3" ) - .arg( mQuery ) - .arg( quotedIdentifier( iter->name() ) ) - .arg( type ); - QgsDebugMsg( sql ); - - //send sql statement and do error handling - PGresult *result = connectionRW->PQexec( sql ); - if ( result == 0 || PQresultStatus( result ) == PGRES_FATAL_ERROR ) - throw PGException( result ); - PQclear( result ); - - if ( !iter->comment().isEmpty() ) - { - sql = QString( "COMMENT ON COLUMN %1.%2 IS %3" ) - .arg( mQuery ) - .arg( quotedIdentifier( iter->name() ) ) - .arg( quotedValue( iter->comment() ) ); - result = connectionRW->PQexec( sql ); - if ( result == 0 || PQresultStatus( result ) == PGRES_FATAL_ERROR ) - throw PGException( result ); - PQclear( result ); - } - } - - connectionRW->PQexecNR( "COMMIT" ); - } - catch ( PGException &e ) - { - e.showErrorMessage( tr( "Error while adding attributes" ) ); - connectionRW->PQexecNR( "ROLLBACK" ); - returnvalue = false; - } - - rewind(); - return returnvalue; -} - -bool QgsPostgresProvider::deleteAttributes( const QgsAttributeIds& ids ) -{ - bool returnvalue = true; - - if ( isQuery ) - return false; - - if ( !connectRW() ) - return false; - - try - { - connectionRW->PQexecNR( "BEGIN" ); - - for ( QgsAttributeIds::const_iterator iter = ids.begin(); iter != ids.end(); ++iter ) - { - QgsFieldMap::const_iterator field_it = attributeFields.find( *iter ); - if ( field_it == attributeFields.constEnd() ) - continue; - - QString column = field_it->name(); - QString sql = QString( "ALTER TABLE %1 DROP COLUMN %2" ) - .arg( mQuery ) - .arg( quotedIdentifier( column ) ); - - //send sql statement and do error handling - PGresult *result = connectionRW->PQexec( sql ); - if ( result == 0 || PQresultStatus( result ) == PGRES_FATAL_ERROR ) - throw PGException( result ); - PQclear( result ); - - //delete the attribute from attributeFields - attributeFields.remove( *iter ); - } - - connectionRW->PQexecNR( "COMMIT" ); - } - catch ( PGException &e ) - { - e.showErrorMessage( tr( "Error while deleting attributes" ) ); - connectionRW->PQexecNR( "ROLLBACK" ); - returnvalue = false; - } - - rewind(); - return returnvalue; -} - -bool QgsPostgresProvider::changeAttributeValues( const QgsChangedAttributesMap & attr_map ) -{ - bool returnvalue = true; - - if ( isQuery ) - return false; - - if ( !connectRW() ) - return false; - - try - { - connectionRW->PQexecNR( "BEGIN" ); - - // cycle through the features - for ( QgsChangedAttributesMap::const_iterator iter = attr_map.begin(); iter != attr_map.end(); ++iter ) - { - QgsFeatureId fid = iter.key(); - - // skip added features - if ( FID_IS_NEW( fid ) ) - continue; - - QString sql = QString( "UPDATE %1 SET " ).arg( mQuery ); - bool first = true; - - const QgsAttributeMap& attrs = iter.value(); - - // cycle through the changed attributes of the feature - for ( QgsAttributeMap::const_iterator siter = attrs.begin(); siter != attrs.end(); ++siter ) - { - try - { - QgsField fld = field( siter.key() ); - - if ( !first ) - sql += ","; - else - first = false; - - sql += QString( "%1=" ).arg( quotedIdentifier( fld.name() ) ); - - if ( fld.typeName() == "geometry" ) - { - sql += QString( "%1(%2)" ) - .arg( connectionRO->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" ) - .arg( quotedValue( siter->toString() ) ); - } - else if ( fld.typeName() == "geography" ) - { - sql += QString( "st_geographyfromewkt(%1)" ) - .arg( quotedValue( siter->toString() ) ); - } - else - { - sql += quotedValue( siter->toString() ); - } - } - catch ( PGFieldNotFound ) - { - // Field was missing - shouldn't happen - } - } - - sql += QString( " WHERE %1" ).arg( whereClause( fid ) ); - - PGresult *result = connectionRW->PQexec( sql ); - if ( result == 0 || PQresultStatus( result ) == PGRES_FATAL_ERROR ) - throw PGException( result ); - PQclear( result ); - } - - connectionRW->PQexecNR( "COMMIT" ); - } - catch ( PGException &e ) - { - e.showErrorMessage( tr( "Error while changing attributes" ) ); - connectionRW->PQexecNR( "ROLLBACK" ); - returnvalue = false; - } - - rewind(); - - return returnvalue; -} - -void QgsPostgresProvider::appendGeomString( QgsGeometry *geom, QString &geomString ) const -{ - unsigned char *buf = geom->asWkb(); - for ( uint i = 0; i < geom->wkbSize(); ++i ) - { - if ( connectionRW->useWkbHex() ) - geomString += QString( "%1" ).arg(( int ) buf[i], 2, 16, QChar( '0' ) ); - else - geomString += QString( "\\%1" ).arg(( int ) buf[i], 3, 8, QChar( '0' ) ); - } -} - -bool QgsPostgresProvider::changeGeometryValues( QgsGeometryMap & geometry_map ) -{ - QgsDebugMsg( "entering." ); - - if ( isQuery || geometryColumn.isNull() ) - return false; - - if ( !connectRW() ) - return false; - - bool returnvalue = true; - - try - { - // Start the PostGIS transaction - connectionRW->PQexecNR( "BEGIN" ); - - QString update = QString( "UPDATE %1 SET %2=%3($1%4,%5) WHERE %6=$2" ) - .arg( mQuery ) - .arg( quotedIdentifier( geometryColumn ) ) - .arg( connectionRW->majorVersion() < 2 ? "geomfromwkb" : "st_geomfromwkb" ) - .arg( connectionRW->useWkbHex() ? "" : "::bytea" ) - .arg( srid ) - .arg( quotedIdentifier( primaryKey ) ); - - PGresult *stmt = connectionRW->PQprepare( "updatefeatures", update, 2, NULL ); - if ( stmt == 0 || PQresultStatus( stmt ) == PGRES_FATAL_ERROR ) - throw PGException( stmt ); - PQclear( stmt ); - - for ( QgsGeometryMap::iterator iter = geometry_map.begin(); - iter != geometry_map.end(); - ++iter ) - { - - QgsDebugMsg( "iterating over the map of changed geometries..." ); - - if ( iter->asWkb() ) - { - QgsDebugMsg( "iterating over feature id " + FID_TO_STRING( iter.key() ) ); - - QString geomParam; - appendGeomString( &*iter, geomParam ); - - QStringList params; - params << geomParam; - if ( primaryKeyType != "tid" ) - { - params << FID_TO_STRING( iter.key() ); - } - else - { - params << QString( "(%1,%2)" ).arg( FID_TO_NUMBER( iter.key() ) >> 16 ).arg( FID_TO_NUMBER( iter.key() ) & 0xffff ); - } - - PGresult *result = connectionRW->PQexecPrepared( "updatefeatures", params ); - if ( result == 0 || PQresultStatus( result ) == PGRES_FATAL_ERROR ) - throw PGException( result ); - PQclear( result ); - } // if (*iter) - - } // for each feature - - connectionRW->PQexecNR( "DEALLOCATE updatefeatures" ); - connectionRW->PQexecNR( "COMMIT" ); - } - catch ( PGException &e ) - { - e.showErrorMessage( tr( "Error while changing geometry values" ) ); - connectionRW->PQexecNR( "ROLLBACK" ); - connectionRW->PQexecNR( "DEALLOCATE updatefeatures" ); - returnvalue = false; - } - - rewind(); - - QgsDebugMsg( "exiting." ); - - return returnvalue; -} - -QgsAttributeList QgsPostgresProvider::attributeIndexes() -{ - QgsAttributeList attributes; - for ( QgsFieldMap::const_iterator it = attributeFields.constBegin(); it != attributeFields.constEnd(); ++it ) - { - attributes.push_back( it.key() ); - } - return attributes; -} - - -int QgsPostgresProvider::capabilities() const -{ - return enabledCapabilities; -} - -bool QgsPostgresProvider::setSubsetString( QString theSQL, bool updateFeatureCount ) -{ - QString prevWhere = sqlWhereClause; - - sqlWhereClause = theSQL.trimmed(); - - QString sql = QString( "select * from %1" ).arg( mQuery ); - - if ( !sqlWhereClause.isEmpty() ) - { - sql += QString( " where %1" ).arg( sqlWhereClause ); - } - - sql += " limit 0"; - - Result res = connectionRO->PQexec( sql ); - if ( PQresultStatus( res ) != PGRES_COMMAND_OK && PQresultStatus( res ) != PGRES_TUPLES_OK ) - { - pushError( QString::fromUtf8( PQresultErrorMessage( res ) ) ); - sqlWhereClause = prevWhere; - return false; - } - - if ( !mIsDbPrimaryKey && !uniqueData( mQuery, primaryKey ) ) - { - sqlWhereClause = prevWhere; - return false; - } - - // Update datasource uri too - mUri.setSql( theSQL ); - // Update yet another copy of the uri. Why are there 3 copies of the - // uri? Perhaps this needs some rationalisation..... - setDataSourceUri( mUri.uri() ); - - if ( updateFeatureCount ) - { - featuresCounted = -1; - } - layerExtent.setMinimal(); - - return true; -} - -/** - * Return the feature count - */ -long QgsPostgresProvider::featureCount() const -{ - if ( featuresCounted >= 0 ) - return featuresCounted; - - // get total number of features - QString sql; - - // use estimated metadata even when there is a where clause, - // although we get an incorrect feature count for the subset - // - but make huge dataset usable. - if ( !isQuery && mUseEstimatedMetadata ) - { - sql = QString( "select reltuples::int from pg_catalog.pg_class where oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); - } - else - { - sql = QString( "select count(*) from %1" ).arg( mQuery ); - - if ( !sqlWhereClause.isEmpty() ) - { - sql += " where " + sqlWhereClause; - } - } - - Result result = connectionRO->PQexec( sql ); - - QgsDebugMsg( "number of features as text: " + - QString::fromUtf8( PQgetvalue( result, 0, 0 ) ) ); - - featuresCounted = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ).toLong(); - - QgsDebugMsg( "number of features: " + QString::number( featuresCounted ) ); - - return featuresCounted; -} - -QgsRectangle QgsPostgresProvider::extent() -{ - if ( geometryColumn.isNull() ) - return QgsRectangle(); - - if ( isGeography ) - return QgsRectangle( -180.0, -90.0, 180.0, 90.0 ); - - if ( layerExtent.isEmpty() ) - { - QString sql; - Result result; - QString ext; - - // get the extents - if ( !isQuery && ( mUseEstimatedMetadata || sqlWhereClause.isEmpty() ) ) - { - // do stats exists? - sql = QString( "SELECT COUNT(*) FROM pg_stats WHERE schemaname=%1 AND tablename=%2 AND attname=%3" ) - .arg( quotedValue( mSchemaName ) ) - .arg( quotedValue( mTableName ) ) - .arg( quotedValue( geometryColumn ) ); - result = connectionRO->PQexec( sql ); - if ( PQresultStatus( result ) == PGRES_TUPLES_OK && PQntuples( result ) == 1 ) - { - if ( QString::fromUtf8( PQgetvalue( result, 0, 0 ) ).toInt() > 0 ) - { - sql = QString( "select reltuples::int from pg_catalog.pg_class where oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); - result = connectionRO->PQexec( sql ); - if ( PQresultStatus( result ) == PGRES_TUPLES_OK && - PQntuples( result ) == 1 && - QString::fromUtf8( PQgetvalue( result, 0, 0 ) ).toLong() > 0 ) - { - sql = QString( "select %1(%2,%3,%4)" ) - .arg( connectionRO->majorVersion() < 2 ? "estimated_extent" : "st_estimated_extent" ) - .arg( quotedValue( mSchemaName ) ) - .arg( quotedValue( mTableName ) ) - .arg( quotedValue( geometryColumn ) ); - result = connectionRO->PQexec( sql ); - if ( PQresultStatus( result ) == PGRES_TUPLES_OK && PQntuples( result ) == 1 ) - { - ext = PQgetvalue( result, 0, 0 ); - - // fix for what might be a postgis bug: when the extent crosses the - // dateline extent() returns -180 to 180 (which appears right), but - // estimated_extent() returns eastern bound of data (>-180) and - // 180 degrees. - if ( !ext.startsWith( "-180 " ) && ext.contains( ",180 " ) ) - { - ext.clear(); - } - } - } - else - { - // no features => ignore estimated extent - ext.clear(); - } - } - } - else - { - QgsDebugMsg( QString( "no column statistics for %1.%2.%3" ).arg( mSchemaName ).arg( mTableName ).arg( geometryColumn ) ); - } - } - - if ( ext.isEmpty() ) - { - sql = QString( "select %1(%2) from %3" ) - .arg( connectionRO->majorVersion() < 2 ? "extent" : "st_extent" ) - .arg( quotedIdentifier( geometryColumn ) ) - .arg( mQuery ); - - if ( !sqlWhereClause.isEmpty() ) - sql += QString( " where %1" ).arg( sqlWhereClause ); - - result = connectionRO->PQexec( sql ); - if ( PQresultStatus( result ) != PGRES_TUPLES_OK ) - connectionRO->PQexecNR( "ROLLBACK" ); - else if ( PQntuples( result ) == 1 ) - ext = PQgetvalue( result, 0, 0 ); - } - - QgsDebugMsg( "Got extents using: " + sql ); - - QRegExp rx( "\\((.+) (.+),(.+) (.+)\\)" ); - if ( ext.contains( rx ) ) - { - QStringList ex = rx.capturedTexts(); - - layerExtent.setXMinimum( ex[1].toDouble() ); - layerExtent.setYMinimum( ex[2].toDouble() ); - layerExtent.setXMaximum( ex[3].toDouble() ); - layerExtent.setYMaximum( ex[4].toDouble() ); - } - else - { - QgsMessageLog::logMessage( tr( "extents query failed: %1" ).arg( ext ), tr( "PostgreSQL" ) ); - } - - QgsDebugMsg( "Set extents to: " + layerExtent.toString() ); - } - - return layerExtent; -} - - -bool QgsPostgresProvider::deduceEndian() -{ - // need to store the PostgreSQL endian format used in binary cursors - // since it appears that starting with - // version 7.4, binary cursors return data in XDR whereas previous versions - // return data in the endian of the server - - QString oidValue; - QString query; - - if ( isQuery ) - { - QString sql = QString( "select * from %1 limit 0" ).arg( mQuery ); - Result res = connectionRO->PQexec( sql ); - - // loop through the returned fields to get a valid oid - int i; - for ( i = 0; i < PQnfields( res ); i++ ) - { - int tableoid = PQftable( res, i ); - if ( tableoid > 0 ) - { - oidValue = QString::number( tableoid ); - break; - } - } - - if ( i < PQnfields( res ) ) - { - // get the table name - res = connectionRO->PQexec( QString( "SELECT pg_namespace.nspname,pg_class.relname FROM pg_class,pg_namespace WHERE pg_class.relnamespace=pg_namespace.oid AND pg_class.oid=%1" ).arg( oidValue ) ); - QString schemaName = QString::fromUtf8( PQgetvalue( res, 0, 0 ) ); - QString tableName = QString::fromUtf8( PQgetvalue( res, 0, 1 ) ); - query = quotedIdentifier( schemaName ) + "." + quotedIdentifier( tableName ); - } - else - { - QgsMessageLog::logMessage( tr( "no oid found" ), tr( "PostgreSQL" ) ); - return false; - } - } - else - { - QString firstOid = QString( "select regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); - Result oidResult = connectionRO->PQexec( firstOid ); - // get the int value from a "normal" select - oidValue = QString::fromUtf8( PQgetvalue( oidResult, 0, 0 ) ); - query = mQuery; - } - - QgsDebugMsg( "Creating binary cursor" ); - - // get the same value using a binary cursor - connectionRO->openCursor( "oidcursor", QString( "select regclass(%1)::oid" ).arg( quotedValue( query ) ) ); - - QgsDebugMsg( "Fetching a record and attempting to get check endian-ness" ); - - Result fResult = connectionRO->PQexec( "fetch forward 1 from oidcursor" ); - swapEndian = true; - if ( PQntuples( fResult ) > 0 ) - { - // get the oid value from the binary cursor - qint64 oid = getBinaryInt( fResult, 0, 0 ); - - QgsDebugMsg( QString( "Got oid of %1 from the binary cursor" ).arg( oid ) ); - QgsDebugMsg( QString( "First oid is %1" ).arg( oidValue ) ); - - // compare the two oid values to determine if we need to do an endian swap - if ( oid != oidValue.toLongLong() ) - swapEndian = false; - } - connectionRO->closeCursor( "oidcursor" ); - return swapEndian; -} - -bool QgsPostgresProvider::getGeometryDetails() -{ - if ( geometryColumn.isNull() ) - { - geomType = QGis::WKBNoGeometry; - valid = true; - return true; - } - - QString fType( "" ); - srid = ""; - valid = false; - QStringList log; - - Result result; - QString sql; - - QString schemaName = mSchemaName; - QString tableName = mTableName; - QString geomCol = geometryColumn; - - if ( isQuery ) - { - sql = QString( "select %1 from %2 limit 0" ).arg( quotedIdentifier( geometryColumn ) ).arg( mQuery ); - - QgsDebugMsg( "Getting geometry column: " + sql ); - - Result result = connectionRO->PQexec( sql ); - if ( PGRES_TUPLES_OK == PQresultStatus( result ) ) - { - Oid tableoid = PQftable( result, 0 ); - int column = PQftablecol( result, 0 ); - - result = connectionRO->PQexec( sql ); - if ( tableoid > 0 && PGRES_TUPLES_OK == PQresultStatus( result ) ) - { - sql = QString( "SELECT pg_namespace.nspname,pg_class.relname FROM pg_class,pg_namespace WHERE pg_class.relnamespace=pg_namespace.oid AND pg_class.oid=%1" ).arg( tableoid ); - result = connectionRO->PQexec( sql ); - - if ( PGRES_TUPLES_OK == PQresultStatus( result ) && 1 == PQntuples( result ) ) - { - schemaName = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - tableName = QString::fromUtf8( PQgetvalue( result, 0, 1 ) ); - - sql = QString( "SELECT attname FROM pg_attribute WHERE attrelid=%1 AND attnum=%2" ).arg( tableoid ).arg( column ); - result = connectionRO->PQexec( sql ); - if ( PGRES_TUPLES_OK == PQresultStatus( result ) && 1 == PQntuples( result ) ) - { - geomCol = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - } - else - { - schemaName = mSchemaName; - tableName = mTableName; - } - } - } - } - } - - sql = QString( "select upper(type),srid from geometry_columns" - " where f_table_name=%1 and f_geometry_column=%2 and f_table_schema=%3" ) - .arg( quotedValue( tableName ) ) - .arg( quotedValue( geomCol ) ) - .arg( quotedValue( schemaName ) ); - - QgsDebugMsg( "Getting geometry column: " + sql ); - result = connectionRO->PQexec( sql ); - - QgsDebugMsg( QString( "geometry column query returned %1 rows" ).arg( PQntuples( result ) ) ); - - if ( PQntuples( result ) > 0 ) - { - fType = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - srid = QString::fromUtf8( PQgetvalue( result, 0, 1 ) ); - } - - if ( srid.isEmpty() || fType.isEmpty() ) - { - sql = QString( "select upper(type),srid from geography_columns" - " where f_table_name=%1 and f_geography_column=%2 and f_table_schema=%3" ) - .arg( quotedValue( tableName ) ) - .arg( quotedValue( geomCol ) ) - .arg( quotedValue( schemaName ) ); - - QgsDebugMsg( "Getting geography column: " + sql ); - result = connectionRO->PQexec( sql ); - - QgsDebugMsg( "geography column query returned " + QString::number( PQntuples( result ) ) ); - - if ( PQntuples( result ) > 0 ) - { - fType = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - srid = QString::fromUtf8( PQgetvalue( result, 0, 1 ) ); - - isGeography = true; - } - } - - if ( srid.isEmpty() || fType.isEmpty() ) - { - // Didn't find what we need in the geometry_columns table, so - // get stuff from the relevant column instead. This may (will?) - // fail if there is no data in the relevant table. - sql = QString( "select %1(%2),upper(geometrytype(%2)) from %3" ) - .arg( connectionRO->majorVersion() < 2 ? "srid" : "st_srid" ) - .arg( quotedIdentifier( geometryColumn ) ) - .arg( mQuery ); - - //it is possible that the where clause restricts the feature type - if ( !sqlWhereClause.isEmpty() ) - { - sql += " WHERE " + sqlWhereClause; - } - - sql += " limit 1"; - - result = connectionRO->PQexec( sql ); - - if ( PQntuples( result ) > 0 ) - { - srid = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - fType = QString::fromUtf8( PQgetvalue( result, 0, 1 ) ); - } - } - - if ( !srid.isEmpty() && !fType.isEmpty() ) - { - valid = true; - if ( fType == "GEOMETRY" ) - { - // check to see if there is a unique geometry type - sql = QString( "select distinct " - "case" - " when upper(geometrytype(%1)) IN ('POINT','MULTIPOINT') THEN 'POINT'" - " when upper(geometrytype(%1)) IN ('LINESTRING','MULTILINESTRING') THEN 'LINESTRING'" - " when upper(geometrytype(%1)) IN ('POLYGON','MULTIPOLYGON') THEN 'POLYGON'" - " end " - "from " ).arg( quotedIdentifier( geometryColumn ) ); - if ( mUseEstimatedMetadata ) - { - sql += QString( "(select %1 from %2 where %1 is not null" ) - .arg( quotedIdentifier( geometryColumn ) ) - .arg( mQuery ); - if ( !sqlWhereClause.isEmpty() ) - sql += " and " + sqlWhereClause; - sql += QString( " limit %1 ) as t" ).arg( sGeomTypeSelectLimit ); - } - else - { - sql += mQuery; - if ( !sqlWhereClause.isEmpty() ) - sql += " where " + sqlWhereClause; - } - - result = connectionRO->PQexec( sql ); - - if ( PQntuples( result ) == 1 && !PQgetisnull( result, 0, 0 ) ) - { - fType = QString::fromUtf8( PQgetvalue( result, 0, 0 ) ); - } - } - if ( fType == "POINT" || fType == "POINTM" ) - { - geomType = QGis::WKBPoint; - } - else if ( fType == "MULTIPOINT" || fType == "MULTIPOINTM" ) - { - geomType = QGis::WKBMultiPoint; - } - else if ( fType == "LINESTRING" || fType == "LINESTRINGM" ) - { - geomType = QGis::WKBLineString; - } - else if ( fType == "MULTILINESTRING" || fType == "MULTILINESTRINGM" ) - { - geomType = QGis::WKBMultiLineString; - } - else if ( fType == "POLYGON" || fType == "POLYGONM" ) - { - geomType = QGis::WKBPolygon; - } - else if ( fType == "MULTIPOLYGON" || fType == "MULTIPOLYGONM" ) - { - geomType = QGis::WKBMultiPolygon; - } - else - { - showMessageBox( tr( "Unknown geometry type" ), - tr( "Column %1 in %2 has a geometry type of %3, which Quantum GIS does not currently support." ) - .arg( geometryColumn ).arg( mQuery ).arg( fType ) ); - valid = false; - } - } - else // something went wrong... - { - log.prepend( tr( "Quantum GIS was unable to determine the type and srid of column %1 in %2. The database communication log was:\n%3" ) - .arg( geometryColumn ) - .arg( mQuery ) - .arg( QString::fromUtf8( PQresultErrorMessage( result ) ) ) ); - showMessageBox( tr( "Unable to get feature type and srid" ), log ); - } - - // store whether the geometry includes measure value - if ( fType == "POINTM" || fType == "MULTIPOINTM" || - fType == "LINESTRINGM" || fType == "MULTILINESTRINGM" || - fType == "POLYGONM" || fType == "MULTIPOLYGONM" ) - { - // explicitly disable adding new features and editing of geometries - // as this would lead to corruption of measures - enabledCapabilities &= ~( QgsVectorDataProvider::ChangeGeometries | QgsVectorDataProvider::AddFeatures ); - } - - - if ( valid ) - { - QgsDebugMsg( "SRID is " + srid ); - QgsDebugMsg( "type is " + fType ); - QgsDebugMsg( "Feature type is " + QString::number( geomType ) ); - QgsDebugMsg( "Feature type name is " + QString( QGis::qgisFeatureTypes[geomType] ) ); - QgsDebugMsg( "Geometry is geography " + isGeography ); - } - else - { - QgsMessageLog::logMessage( tr( "Failed to get geometry details for PostGIS column %1.%2." ).arg( tableName ).arg( geometryColumn ), tr( "PostgreSQL" ) ); - } - - return valid; -} - -QString QgsPostgresProvider::quotedIdentifier( QString ident ) -{ - ident.replace( '"', "\"\"" ); - return ident.prepend( "\"" ).append( "\"" ); -} - -QString QgsPostgresProvider::quotedValue( QString value ) -{ - if ( value.isNull() ) - return "NULL"; - - value.replace( "'", "''" ); - value.replace( "\\\"", "\\\\\"" ); - return value.prepend( "'" ).append( "'" ); -} - -PGresult *QgsPostgresProvider::Conn::PQexec( QString query ) -{ - QgsDebugMsgLevel( QString( "Executing SQL: %1" ).arg( query ), 3 ); - PGresult *res = ::PQexec( conn, query.toUtf8() ); - - if ( res ) - { - int errorStatus = PQresultStatus( res ); - if ( errorStatus != PGRES_COMMAND_OK && errorStatus != PGRES_TUPLES_OK ) - { - QgsMessageLog::logMessage( tr( "Errornous query: %1 returned %2 [%3]" ) - .arg( query ).arg( errorStatus ).arg( PQresultErrorMessage( res ) ), - tr( "PostgreSQL" ) ); - } - } - else - { - QgsMessageLog::logMessage( tr( "Query failed: %1\nError: %2" ).arg( query ), tr( "PostgreSQL" ) ); - } - - return res; -} - -bool QgsPostgresProvider::Conn::openCursor( QString cursorName, QString sql ) -{ - if ( openCursors++ == 0 ) - { - QgsDebugMsg( "Starting read-only transaction" ); - PQexecNR( "BEGIN READ ONLY" ); - } - QgsDebugMsgLevel( QString( "Binary cursor %1 for %2" ).arg( cursorName ).arg( sql ), 3 ); - return PQexecNR( QString( "declare %1 binary cursor for %2" ).arg( cursorName ).arg( sql ) ); -} - -bool QgsPostgresProvider::Conn::closeCursor( QString cursorName ) -{ - if ( !PQexecNR( QString( "CLOSE %1" ).arg( cursorName ) ) ) - return false; - - if ( --openCursors == 0 ) - { - QgsDebugMsg( "Committing read-only transaction" ); - PQexecNR( "COMMIT" ); - } - - return true; -} - -bool QgsPostgresProvider::Conn::PQexecNR( QString query, bool retry ) -{ - Result res = ::PQexec( conn, query.toUtf8() ); - if ( !res ) - { - QgsMessageLog::logMessage( tr( "Query: %1 returned no result buffer" ).arg( query ), tr( "PostgreSQL" ) ); - return false; - } - - ExecStatusType errorStatus = PQresultStatus( res ); - if ( errorStatus == PGRES_COMMAND_OK ) - return true; - - QgsMessageLog::logMessage( tr( "Query: %1 returned %2 [%3]" ) - .arg( query ) - .arg( errorStatus ) - .arg( QString::fromUtf8( PQresultErrorMessage( res ) ) ), - tr( "PostgreSQL" ) ); - - if ( openCursors ) - { - QgsPostgresProvider::showMessageBox( - tr( "Query failed" ), - tr( "%1 cursor states lost.\nSQL: %2\nResult: %3 (%4)" ) - .arg( openCursors ) - .arg( query ) - .arg( errorStatus ) - .arg( QString::fromUtf8( PQresultErrorMessage( res ) ) ) ); - openCursors = 0; - } - - if ( PQstatus( conn ) == CONNECTION_OK ) - { - PQexecNR( "ROLLBACK" ); - } - else if ( retry ) - { - QgsMessageLog::logMessage( tr( "resetting bad connection." ), tr( "PostgreSQL" ) ); - ::PQreset( conn ); - if ( PQstatus( conn ) == CONNECTION_OK ) - { - if ( PQexecNR( query, false ) ) - { - QgsMessageLog::logMessage( tr( "retry after reset succeeded." ), tr( "PostgreSQL" ) ); - return true; - } - else - { - QgsMessageLog::logMessage( tr( "retry after reset failed again." ), tr( "PostgreSQL" ) ); - return false; - } - } - else - { - QgsMessageLog::logMessage( tr( "connection still bad after reset." ), tr( "PostgreSQL" ) ); - } - } - else - { - QgsMessageLog::logMessage( tr( "bad connection, not retrying." ), tr( "PostgreSQL" ) ); - } - - return false; -} - -PGresult *QgsPostgresProvider::Conn::PQgetResult() -{ - return ::PQgetResult( conn ); -} - -PGresult *QgsPostgresProvider::Conn::PQprepare( QString stmtName, QString query, int nParams, const Oid *paramTypes ) -{ - return ::PQprepare( conn, stmtName.toUtf8(), query.toUtf8(), nParams, paramTypes ); -} - -PGresult *QgsPostgresProvider::Conn::PQexecPrepared( QString stmtName, const QStringList ¶ms ) -{ - const char **param = new const char *[ params.size()]; - QList qparam; - - for ( int i = 0; i < params.size(); i++ ) - { - qparam << params[i].toUtf8(); - - if ( params[i].isNull() ) - param[i] = 0; - else - param[i] = qparam[i]; - } - - PGresult *res = ::PQexecPrepared( conn, stmtName.toUtf8(), params.size(), param, NULL, NULL, 0 ); - - delete [] param; - - return res; -} - -void QgsPostgresProvider::Conn::PQfinish() -{ - ::PQfinish( conn ); -} - -int QgsPostgresProvider::Conn::PQsendQuery( QString query ) -{ - return ::PQsendQuery( conn, query.toUtf8() ); -} - -void QgsPostgresProvider::showMessageBox( const QString& title, const QString& text ) -{ - QgsMessageOutput* message = QgsMessageOutput::createMessageOutput(); - message->setTitle( title ); - message->setMessage( text, QgsMessageOutput::MessageText ); - message->showMessage(); -} - -void QgsPostgresProvider::showMessageBox( const QString& title, const QStringList& text ) -{ - showMessageBox( title, text.join( "\n" ) ); -} - - QgsCoordinateReferenceSystem QgsPostgresProvider::crs() { QgsCoordinateReferenceSystem srs; - srs.createFromSrid( srid.toInt() ); + srs.createFromSrid( mRequestedSrid.toInt() ); return srs; } QString QgsPostgresProvider::subsetString() { - return sqlWhereClause; -} - -PGconn *QgsPostgresProvider::pgConnection() -{ - connectRW(); - return connectionRW->pgConnection(); + return mSqlWhereClause; } QString QgsPostgresProvider::getTableName() @@ -4385,21 +3016,17 @@ QString QgsPostgresProvider::getTableName() return mTableName; } - size_t QgsPostgresProvider::layerCount() const { return 1; // XXX need to return actual number of layers } // QgsPostgresProvider::layerCount() - QString QgsPostgresProvider::name() const { return POSTGRES_KEY; } // QgsPostgresProvider::name() - - QString QgsPostgresProvider::description() const { return POSTGRES_DESCRIPTION; @@ -4434,6 +3061,23 @@ QGISEXTERN bool isProvider() { return true; } + +QGISEXTERN QgsPgSourceSelect *selectWidget( QWidget *parent, Qt::WFlags fl ) +{ + return new QgsPgSourceSelect( parent, fl ); +} + +QGISEXTERN int dataCapabilities() +{ + return QgsDataProvider::Database; +} + +QGISEXTERN QgsDataItem *dataItem( QString thePath, QgsDataItem *parentItem ) +{ + Q_UNUSED( thePath ); + return new QgsPGRootItem( parentItem, "PostGIS", "pg:" ); +} + // --------------------------------------------------------------------------- QGISEXTERN QgsVectorLayerImport::ImportError createEmptyLayer( diff --git a/src/providers/postgres/qgspostgresprovider.h b/src/providers/postgres/qgspostgresprovider.h index 327c512ee96..3c04a82558a 100644 --- a/src/providers/postgres/qgspostgresprovider.h +++ b/src/providers/postgres/qgspostgresprovider.h @@ -18,41 +18,20 @@ #ifndef QGSPOSTGRESPROVIDER_H #define QGSPOSTGRESPROVIDER_H -extern "C" -{ -#include -} #include "qgsvectordataprovider.h" #include "qgsrectangle.h" #include "qgsvectorlayerimport.h" - -#include -#include -#include -#include +#include "qgspostgresconn.h" #include +#include class QgsFeature; class QgsField; class QgsGeometry; - #include "qgsdatasourceuri.h" -/** Layer Property structure */ -// TODO: Fill to Postgres/PostGIS specifications -struct QgsPostgresLayerProperty -{ - // Postgres/PostGIS layer properties - QString type; - QString schemaName; - QString tableName; - QString geometryColName; - QStringList pkCols; - QString sql; -}; - /** \class QgsPostgresProvider \brief Data provider for PostgreSQL/PostGIS layers. @@ -85,10 +64,10 @@ class QgsPostgresProvider : public QgsVectorDataProvider * @param uri String containing the required parameters to connect to the database * and query the table. */ - QgsPostgresProvider( QString const & uri = "" ); + QgsPostgresProvider( QString const &uri = "" ); //! Destructor - virtual ~ QgsPostgresProvider(); + virtual ~QgsPostgresProvider(); /** * Returns the permanent storage type for this layer as a friendly name. @@ -164,11 +143,6 @@ class QgsPostgresProvider : public QgsVectorDataProvider */ uint fieldCount() const; - /** - * Get the data source URI structure used by this layer - */ - QgsDataSourceURI& getURI(); - /** * Return a string representation of the endian-ness for the layer */ @@ -184,9 +158,9 @@ class QgsPostgresProvider : public QgsVectorDataProvider */ virtual QgsRectangle extent(); - /** * Get the name of the primary key for the layer + /** Determine the fields making up the primary key */ - QString getPrimaryKey(); + bool determinePrimaryKey(); /** * Get the field information for the layer @@ -264,18 +238,12 @@ class QgsPostgresProvider : public QgsVectorDataProvider /** Changes geometries of existing features - @param geometry_map A std::map containing the feature IDs to change the geometries of. + @param geometry_map A QMap containing the feature IDs to change the geometries of. the second map parameter being the new geometries themselves @return true in case of success and false in case of failure */ bool changeGeometryValues( QgsGeometryMap & geometry_map ); - //! Get the list of supported layers - bool supportedLayers( QVector &layers, - bool searchGeometryColumnsOnly = true, - bool searchPublicOnly = true, - bool allowGeometrylessTables = false ); - //! Get the postgres connection PGconn * pgConnection(); @@ -332,7 +300,6 @@ class QgsPostgresProvider : public QgsVectorDataProvider */ QString description() const; - signals: /** * This is emitted whenever the worker thread has fully calculated the @@ -354,44 +321,26 @@ class QgsPostgresProvider : public QgsVectorDataProvider void repaintRequested(); private: - - int providerId; // id to append to provider specific identified (like cursors) + int mProviderId; // id to append to provider specific identified (like cursors) bool declareCursor( const QString &cursorName, const QgsAttributeList &fetchAttributes, bool fetchGeometry, QString whereClause ); - bool getFeature( PGresult *queryResult, int row, bool fetchGeometry, + bool getFeature( QgsPostgresResult &queryResult, + int row, + bool fetchGeometry, QgsFeature &feature, const QgsAttributeList &fetchAttributes ); + QString pkParamWhereClause( int offset ) const; QString whereClause( QgsFeatureId featureId ) const; - /** Gets information about the spatial tables */ - bool getTableInfo( bool searchGeometryColumnsOnly, bool searchPublicOnly, bool allowGeometrylessTables ); - - /** get primary key candidates (all int4 columns) */ - QStringList pkCandidates( QString schemaName, QString viewName ); - bool hasSufficientPermsAndCapabilities(); - qint64 getBinaryInt( PGresult *queryResult, int row, int col ); - const QgsField &field( int index ) const; - /** Double quote a PostgreSQL identifier for placement in a SQL string. - */ - static QString quotedIdentifier( QString ident ); - - /** Quote a value for placement in a SQL string. - */ - static QString quotedValue( QString value ); - - /** expression to retrieve value - */ - QString fieldExpression( const QgsField &fld ) const; - /** Load the field list */ bool loadFields(); @@ -414,30 +363,27 @@ class QgsPostgresProvider : public QgsVectorDataProvider bool mFetching; // true if a cursor was declared int mFetched; // number of retrieved features - std::vector < QgsFeature > features; - QgsFieldMap attributeFields; + QVector mFeatures; + QgsFieldMap mAttributeFields; QString mDataComment; //! Data source URI struct for this layer QgsDataSourceURI mUri; - //! List of the supported layers - QVector layersSupported; - /** * Flag indicating if the layer data source is a valid PostgreSQL layer */ - bool valid; + bool mValid; /** * provider references query (instead of a table) */ - bool isQuery; + bool mIsQuery; /** * geometry is geography */ - bool isGeography; + bool mIsGeography; /** * Name of the table with no schema @@ -458,327 +404,104 @@ class QgsPostgresProvider : public QgsVectorDataProvider /** * SQL statement used to limit the features retrieved */ - QString sqlWhereClause; + QString mSqlWhereClause; - /** - * Primary key column for fetching features. If there is no primary key - * the oid is used to fetch features. - */ - QString primaryKey; - /** - * Primary key column is "real" primary key - */ - bool mIsDbPrimaryKey; /** * Data type for the primary key */ - QString primaryKeyType; - /** - * Name of the geometry column in the table - */ - QString geometryColumn; - /** - * Geometry type - */ - QGis::WkbType geomType; + enum { pktUnknown, pktInt, pktTid, pktOid, pktFidMap } mPrimaryKeyType; /** - * Spatial reference id of the layer + * List of primary key attributes for fetching features. */ - QString srid; - /** - * Rectangle that contains the extent (bounding box) of the layer - */ - QgsRectangle layerExtent; + QList mPrimaryKeyAttrs; + QString mPrimaryKeyDefault; - /** - * Number of features in the layer - */ - mutable long featuresCounted; + QString mGeometryColumn; //! name of the geometry column + QgsRectangle mLayerExtent; //! Rectangle that contains the extent (bounding box) of the layer + mutable long mFeaturesCounted; //! Number of features in the layer + + QGis::WkbType mDetectedGeomType; //! geometry type detected in the database + QGis::WkbType mRequestedGeomType; //! geometry type requested in the uri + QString mDetectedSrid; //! Spatial reference detected in the database + QString mRequestedSrid; //! Spatial reference requested in the uri /** * Feature queue that GetNextFeature will retrieve from * before the next fetch from PostgreSQL */ - std::queue mFeatureQueue; + QQueue mFeatureQueue; - /** - * Maximal size of the feature queue - */ - int mFeatureQueueSize; + int mFeatureQueueSize; //! Maximal size of the feature queue - /** - * Flag indicating whether data from binary cursors must undergo an - * endian conversion prior to use - @note - - XXX Umm, it'd be helpful to know what we're swapping from and to. - XXX Presumably this means swapping from big-endian (network) byte order - XXX to little-endian; but the inverse transaction is possible, too, and - XXX that's not reflected in this variable - */ - bool swapEndian; - - bool deduceEndian(); bool getGeometryDetails(); /* Use estimated metadata. Uses fast table counts, geometry type and extent determination */ bool mUseEstimatedMetadata; - /* Disable support for SelectAtId */ - bool mSelectAtIdDisabled; + bool mSelectAtIdDisabled; //! Disable support for SelectAtId - // Produces a QMessageBox with the given title and text. Doesn't - // return until the user has dismissed the dialog box. - static void showMessageBox( const QString& title, const QString &text ); - static void showMessageBox( const QString& title, const QStringList &text ); - - // A simple class to store the rows of the sql executed in the - // findColumns() function. - class TT - { - public: - TT() {}; - - QString view_schema; - QString view_name; - QString view_column_name; - QString table_schema; - QString table_name; - QString column_name; - QString table_type; - QString column_type; - }; - - struct PGFieldNotFound - { - }; + struct PGFieldNotFound {}; //! Exception to throw struct PGException { - PGException( PGresult *r ) : result( r ) - { - } + PGException( QgsPostgresResult &r ) + : mWhat( r.PQresultErrorMessage() ) + {} - PGException( const PGException &e ) : result( e.result ) - { - } + PGException( const PGException &e ) + : mWhat( e.errorMessage() ) + {} ~PGException() - { - if ( result ) - PQclear( result ); - } + {} QString errorMessage() const { - return result ? - QString::fromUtf8( PQresultErrorMessage( result ) ) : - tr( "unexpected PostgreSQL error" ); - } - - void showErrorMessage( QString title ) const - { - showMessageBox( title, errorMessage() ); + return mWhat; } private: - PGresult *result; + QString mWhat; }; - // A simple class to store four strings - class SRC - { - public: - SRC() {}; - SRC( QString s, QString r, QString c, QString t ) : - schema( s ), relation( r ), column( c ), type( t ) {}; - QString schema, relation, column, type; - }; - - // A structure to store the underlying schema.table.column for - // each column in mSchemaName.mTableName - typedef std::map tableCols; - - // A function that chooses a view column that is suitable for use - // a the qgis key column. - QString chooseViewColumn( const tableCols& cols ); - // A function that determines if the given schema.table.column // contains unqiue entries bool uniqueData( QString query, QString colName ); - // Function that populates the given cols structure. - void findColumns( tableCols& cols ); + int mEnabledCapabilities; - /**Helper function that collects information about the origin and type of a view column. - Inputs are information about the column in the underlying table - (from information_schema.view_column_usage), the attribute name - in the view and the view definition. For view columns that refer - to other views, this function calls itself until a table entry is found. - @param ns namespace of underlying table - @param relname name of underlying relation - @param attname attribute name in underlying table - @param viewDefinition definition of this view - @param result - @return 0 in case of success*/ - int SRCFromViewColumn( const QString& ns, const QString& relname, const QString& attname_table, - const QString& attname_view, const QString& viewDefinition, SRC& result ) const; + void appendGeomParam( QgsGeometry *geom, QStringList ¶m ) const; + void appendPkParams( QgsFeatureId fid, QStringList ¶m ) const; - int enabledCapabilities; - - void appendGeomString( QgsGeometry *geom, QString &geomParam ) const; QString paramValue( QString fieldvalue, const QString &defaultValue ) const; - class Conn - { - public: - Conn( PGconn *connection ) - : ref( 1 ) - , openCursors( 0 ) - , conn( connection ) - , gotPostgisVersion( false ) - { - } - - //! get postgis version string - QString postgisVersion(); - - //! get status of GEOS capability - bool hasGEOS(); - - //! get status of topology capability - bool hasTopology(); - - //! get status of GIST capability - bool hasGIST(); - - //! get status of PROJ4 capability - bool hasPROJ(); - - //! encode wkb in hex - bool useWkbHex() { return mUseWkbHex; } - - //! major PostgreSQL version - int majorVersion() { return postgisVersionMajor; } - - //! PostgreSQL version - int pgVersion() { return postgresqlVersion; } - - //! run a query and free result buffer - bool PQexecNR( QString query, bool retry = true ); - - //! cursor handling - bool openCursor( QString cursorName, QString declare ); - bool closeCursor( QString cursorName ); - - PGconn *pgConnection() { return conn; } - - // - // libpq wrapper - // - - // run a query and check for errors - PGresult *PQexec( QString query ); - void PQfinish(); - int PQsendQuery( QString query ); - PGresult *PQgetResult(); - PGresult *PQprepare( QString stmtName, QString query, int nParams, const Oid *paramTypes ); - PGresult *PQexecPrepared( QString stmtName, const QStringList ¶ms ); - - static Conn *connectDb( const QString &conninfo, bool readonly ); - static void disconnectRW( Conn *&conn ); - static void disconnectRO( Conn *&conn ); - static void disconnect( QMap &connections, Conn *&conn ); - - private: - int ref; - int openCursors; - PGconn *conn; - - //! GEOS capability - bool geosAvailable; - - //! Topology capability - bool topologyAvailable; - - //! PostGIS version string - QString postgisVersionInfo; - - //! Are postgisVersionMajor, postgisVersionMinor, geosAvailable, gistAvailable, projAvailable, topologyAvailable valid? - bool gotPostgisVersion; - - //! PostgreSQL version - int postgresqlVersion; - - //! PostGIS major version - int postgisVersionMajor; - - //! PostGIS minor version - int postgisVersionMinor; - - //! GIST capability - bool gistAvailable; - - //! PROJ4 capability - bool projAvailable; - - //! encode wkb in hex - bool mUseWkbHex; - - static QMap connectionsRW; - static QMap connectionsRO; - static QMap passwordCache; - }; - - class Result - { - public: - Result( PGresult *theRes = 0 ) : res( theRes ) {} - ~Result() { if ( res ) PQclear( res ); } - - operator PGresult *() { return res; } - - Result &operator=( PGresult *theRes ) { if ( res ) PQclear( res ); res = theRes; return *this; } - - private: - PGresult *res; - }; - - /** - * Connection pointers - */ - Conn *connectionRO; - Conn *connectionRW; + QgsPostgresConn *mConnectionRO; //! read-only database connection (initially) + QgsPostgresConn *mConnectionRW; //! read-write database connection (on update) + //! establish read-write connection bool connectRW() { - if ( connectionRW ) - return connectionRW; + if ( mConnectionRW ) + return mConnectionRW; - connectionRW = Conn::connectDb( mUri.connectionInfo(), false ); + mConnectionRW = QgsPostgresConn::connectDb( mUri.connectionInfo(), false ); - return connectionRW; + return mConnectionRW; } void disconnectDb(); - static int providerIds; + static QString quotedIdentifier( QString ident ) { return QgsPostgresConn::quotedIdentifier( ident ); } + static QString quotedValue( QVariant value ) { return QgsPostgresConn::quotedValue( value ); } - QString primaryKeyDefault(); - void parseView(); - - /** - * Default value for primary key - */ - QString mPrimaryKeyDefault; - -#if 0 - /** used to cache the lastest fetched features */ - QHash mFeatureMap; - QList mPriorityIds; -#endif + static int sProviderIds; + static const int sFeatureQueueSize; + QMap mKeyToFid; // map key values to feature id + QMap mFidToKey; // map feature back to fea + QgsFeatureId mFidCounter; // next feature id if map is used + QgsFeatureId lookupFid( const QVariant &v ); // lookup existing mapping or add a new one }; #endif diff --git a/src/providers/postgres/create_test_tables b/tests/testdata/create_test_tables similarity index 100% rename from src/providers/postgres/create_test_tables rename to tests/testdata/create_test_tables