/*************************************************************************** qgspostgresprovider.cpp - QGIS data provider for PostgreSQL/PostGIS layers ------------------- begin : 2004/01/07 copyright : (C) 2004 by Gary E.Sherman email : sherman at mrcc.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 "qgsapplication.h" #include "qgsfeature.h" #include "qgsfield.h" #include "qgsgeometry.h" #include "qgsmessageoutput.h" #include "qgsmessagelog.h" #include "qgsprojectstorageregistry.h" #include "qgsrectangle.h" #include "qgscoordinatereferencesystem.h" #include "qgsxmlutils.h" #include "qgsvectorlayer.h" #include #include "qgsvectorlayerexporter.h" #include "qgspostgresprovider.h" #include "qgspostgresconn.h" #include "qgspostgresconnpool.h" #include "qgspostgresdataitems.h" #include "qgspostgresfeatureiterator.h" #include "qgspostgrestransaction.h" #include "qgspostgreslistener.h" #include "qgspostgresprojectstorage.h" #include "qgslogger.h" #include "qgsfeedback.h" #include "qgssettings.h" #ifdef HAVE_GUI #include "qgspgsourceselect.h" #include "qgssourceselectprovider.h" #endif const QString POSTGRES_KEY = QStringLiteral( "postgres" ); const QString POSTGRES_DESCRIPTION = QStringLiteral( "PostgreSQL/PostGIS data provider" ); static const QString EDITOR_WIDGET_STYLES_TABLE = QStringLiteral( "qgis_editor_widget_styles" ); inline qint64 PKINT2FID( qint32 x ) { return QgsPostgresUtils::int32pk_to_fid( x ); } inline qint32 FID2PKINT( qint64 x ) { return QgsPostgresUtils::fid_to_int32pk( x ); } static bool tableExists( QgsPostgresConn &conn, const QString &name ) { QgsPostgresResult res( conn.PQexec( "SELECT COUNT(*) FROM information_schema.tables WHERE table_name=" + QgsPostgresConn::quotedValue( name ) ) ); return res.PQgetvalue( 0, 0 ).toInt() > 0; } QgsPostgresPrimaryKeyType QgsPostgresProvider::pkType( const QgsField &f ) const { switch ( f.type() ) { case QVariant::LongLong: // unless we can guarantee all values are unsigned // (in which case we could use pktUint64) // we'll have to use a Map type. // See https://issues.qgis.org/issues/14262 return PktFidMap; // pktUint64 case QVariant::Int: return PktInt; default: return PktFidMap; } } QgsPostgresProvider::QgsPostgresProvider( QString const &uri, const ProviderOptions &options ) : QgsVectorDataProvider( uri, options ) , mShared( new QgsPostgresSharedData ) { QgsDebugMsg( QStringLiteral( "URI: %1 " ).arg( 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.wkbType(); if ( mUri.hasParam( QStringLiteral( "checkPrimaryKeyUnicity" ) ) ) { if ( mUri.param( QStringLiteral( "checkPrimaryKeyUnicity" ) ).compare( QLatin1String( "0" ) ) == 0 ) { mCheckPrimaryKeyUnicity = false; } else { mCheckPrimaryKeyUnicity = true; } } if ( mSchemaName.isEmpty() && mTableName.startsWith( '(' ) && mTableName.endsWith( ')' ) ) { mIsQuery = true; mQuery = mTableName; mTableName.clear(); } else { mIsQuery = false; if ( !mSchemaName.isEmpty() ) { mQuery += quotedIdentifier( mSchemaName ) + '.'; } if ( !mTableName.isEmpty() ) { mQuery += quotedIdentifier( mTableName ); } } mUseEstimatedMetadata = mUri.useEstimatedMetadata(); mSelectAtIdDisabled = mUri.selectAtIdDisabled(); QgsDebugMsg( QStringLiteral( "Connection info is %1" ).arg( mUri.connectionInfo( false ) ) ); QgsDebugMsg( QStringLiteral( "Geometry column is: %1" ).arg( mGeometryColumn ) ); QgsDebugMsg( QStringLiteral( "Schema is: %1" ).arg( mSchemaName ) ); QgsDebugMsg( QStringLiteral( "Table name is: %1" ).arg( mTableName ) ); QgsDebugMsg( QStringLiteral( "Query is: %1" ).arg( mQuery ) ); QgsDebugMsg( QStringLiteral( "Where clause is: %1" ).arg( mSqlWhereClause ) ); // no table/query passed, the provider could be used to get tables if ( mQuery.isEmpty() ) { return; } mConnectionRO = QgsPostgresConn::connectDb( mUri.connectionInfo( false ), true ); if ( !mConnectionRO ) { return; } if ( !hasSufficientPermsAndCapabilities() ) // check permissions and set capabilities { disconnectDb(); return; } if ( !getGeometryDetails() ) // gets srid, geometry and data type { // the table is not a geometry table QgsMessageLog::logMessage( tr( "invalid PostgreSQL layer" ), tr( "PostGIS" ) ); disconnectDb(); return; } // NOTE: mValid would be true after true return from // getGeometryDetails, see https://issues.qgis.org/issues/13781 if ( mSpatialColType == SctTopoGeometry ) { if ( !getTopoLayerInfo() ) // gets topology name and layer id { QgsMessageLog::logMessage( tr( "invalid PostgreSQL topology layer" ), tr( "PostGIS" ) ); mValid = false; disconnectDb(); return; } } mLayerExtent.setMinimal(); // set the primary key if ( !determinePrimaryKey() ) { QgsMessageLog::logMessage( tr( "PostgreSQL layer has no primary key." ), tr( "PostGIS" ) ); 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( QStringLiteral( "set client_min_messages to error" ) ); #endif //fill type names into sets QList nativeTypes; nativeTypes // integer types << QgsVectorDataProvider::NativeType( tr( "Whole number (smallint - 16bit)" ), QStringLiteral( "int2" ), QVariant::Int, -1, -1, 0, 0 ) << QgsVectorDataProvider::NativeType( tr( "Whole number (integer - 32bit)" ), QStringLiteral( "int4" ), QVariant::Int, -1, -1, 0, 0 ) << QgsVectorDataProvider::NativeType( tr( "Whole number (integer - 64bit)" ), QStringLiteral( "int8" ), QVariant::LongLong, -1, -1, 0, 0 ) << QgsVectorDataProvider::NativeType( tr( "Decimal number (numeric)" ), QStringLiteral( "numeric" ), QVariant::Double, 1, 20, 0, 20 ) << QgsVectorDataProvider::NativeType( tr( "Decimal number (decimal)" ), QStringLiteral( "decimal" ), QVariant::Double, 1, 20, 0, 20 ) // floating point << QgsVectorDataProvider::NativeType( tr( "Decimal number (real)" ), QStringLiteral( "real" ), QVariant::Double, -1, -1, -1, -1 ) << QgsVectorDataProvider::NativeType( tr( "Decimal number (double)" ), QStringLiteral( "double precision" ), QVariant::Double, -1, -1, -1, -1 ) // string types << QgsVectorDataProvider::NativeType( tr( "Text, fixed length (char)" ), QStringLiteral( "char" ), QVariant::String, 1, 255, -1, -1 ) << QgsVectorDataProvider::NativeType( tr( "Text, limited variable length (varchar)" ), QStringLiteral( "varchar" ), QVariant::String, 1, 255, -1, -1 ) << QgsVectorDataProvider::NativeType( tr( "Text, unlimited length (text)" ), QStringLiteral( "text" ), QVariant::String, -1, -1, -1, -1 ) // date type << QgsVectorDataProvider::NativeType( tr( "Date" ), QStringLiteral( "date" ), QVariant::Date, -1, -1, -1, -1 ) << QgsVectorDataProvider::NativeType( tr( "Time" ), QStringLiteral( "time" ), QVariant::Time, -1, -1, -1, -1 ) << QgsVectorDataProvider::NativeType( tr( "Date & Time" ), QStringLiteral( "timestamp without time zone" ), QVariant::DateTime, -1, -1, -1, -1 ) // complex types << QgsVectorDataProvider::NativeType( tr( "Map (hstore)" ), QStringLiteral( "hstore" ), QVariant::Map, -1, -1, -1, -1, QVariant::String ) << QgsVectorDataProvider::NativeType( tr( "Array of number (integer - 32bit)" ), QStringLiteral( "int4[]" ), QVariant::List, -1, -1, -1, -1, QVariant::Int ) << QgsVectorDataProvider::NativeType( tr( "Array of number (integer - 64bit)" ), QStringLiteral( "int8[]" ), QVariant::List, -1, -1, -1, -1, QVariant::LongLong ) << QgsVectorDataProvider::NativeType( tr( "Array of number (double)" ), QStringLiteral( "double precision[]" ), QVariant::List, -1, -1, -1, -1, QVariant::Double ) << QgsVectorDataProvider::NativeType( tr( "Array of text" ), QStringLiteral( "text[]" ), QVariant::StringList, -1, -1, -1, -1, QVariant::String ) // boolean << QgsVectorDataProvider::NativeType( tr( "Boolean" ), QStringLiteral( "bool" ), QVariant::Bool, -1, -1, -1, -1 ) ; if ( connectionRO()->pgVersion() >= 90200 ) { nativeTypes << QgsVectorDataProvider::NativeType( tr( "JSON (json)" ), QStringLiteral( "json" ), QVariant::Map, -1, -1, -1, -1, QVariant::String ); if ( connectionRO()->pgVersion() >= 90400 ) { nativeTypes << QgsVectorDataProvider::NativeType( tr( "JSON (jsonb)" ), QStringLiteral( "jsonb" ), QVariant::Map, -1, -1, -1, -1, QVariant::String ); } } setNativeTypes( nativeTypes ); QString key; switch ( mPrimaryKeyType ) { case PktOid: key = QStringLiteral( "oid" ); break; case PktTid: key = QStringLiteral( "tid" ); break; case PktInt: case PktUint64: Q_ASSERT( mPrimaryKeyAttrs.size() == 1 ); Q_ASSERT( mPrimaryKeyAttrs[0] >= 0 && mPrimaryKeyAttrs[0] < mAttributeFields.count() ); key = mAttributeFields.at( mPrimaryKeyAttrs.at( 0 ) ).name(); break; case PktFidMap: { QString delim; Q_FOREACH ( int idx, mPrimaryKeyAttrs ) { key += delim + mAttributeFields.at( idx ).name(); delim = ','; } } break; case PktUnknown: QgsMessageLog::logMessage( tr( "PostgreSQL layer has unknown primary key type." ), tr( "PostGIS" ) ); mValid = false; break; } if ( mValid ) { mUri.setKeyColumn( key ); setDataSourceUri( mUri.uri( false ) ); } else { disconnectDb(); } mLayerMetadata.setType( QStringLiteral( "dataset" ) ); mLayerMetadata.setCrs( crs() ); } QgsPostgresProvider::~QgsPostgresProvider() { disconnectDb(); QgsDebugMsg( QStringLiteral( "deconstructing." ) ); } QgsAbstractFeatureSource *QgsPostgresProvider::featureSource() const { return new QgsPostgresFeatureSource( this ); } QgsPostgresConn *QgsPostgresProvider::connectionRO() const { return mTransaction ? mTransaction->connection() : mConnectionRO; } void QgsPostgresProvider::setListening( bool isListening ) { if ( isListening && !mListener ) { mListener.reset( QgsPostgresListener::create( mUri.connectionInfo( false ) ).release() ); connect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify ); } else if ( !isListening && mListener ) { disconnect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify ); mListener.reset(); } } QgsPostgresConn *QgsPostgresProvider::connectionRW() { if ( mTransaction ) { return mTransaction->connection(); } else if ( !mConnectionRW ) { mConnectionRW = QgsPostgresConn::connectDb( mUri.connectionInfo( false ), false ); } return mConnectionRW; } QgsTransaction *QgsPostgresProvider::transaction() const { return static_cast( mTransaction ); } void QgsPostgresProvider::setTransaction( QgsTransaction *transaction ) { // static_cast since layers cannot be added to a transaction of a non-matching provider mTransaction = static_cast( transaction ); } void QgsPostgresProvider::disconnectDb() { if ( mConnectionRO ) { mConnectionRO->unref(); mConnectionRO = nullptr; } if ( mConnectionRW ) { mConnectionRW->unref(); mConnectionRW = nullptr; } } QString QgsPostgresProvider::storageType() const { return QStringLiteral( "PostgreSQL database with PostGIS extension" ); } QgsFeatureIterator QgsPostgresProvider::getFeatures( const QgsFeatureRequest &request ) const { if ( !mValid ) { QgsMessageLog::logMessage( tr( "Read attempt on an invalid PostgreSQL data source" ), tr( "PostGIS" ) ); return QgsFeatureIterator(); } QgsPostgresFeatureSource *featureSrc = static_cast( featureSource() ); return QgsFeatureIterator( new QgsPostgresFeatureIterator( featureSrc, true, request ) ); } QString QgsPostgresProvider::pkParamWhereClause( int offset, const char *alias ) const { QString whereClause; QString aliased; if ( alias ) aliased = QStringLiteral( "%1." ).arg( alias ); switch ( mPrimaryKeyType ) { case PktTid: whereClause = QStringLiteral( "%2ctid=$%1" ).arg( offset ).arg( aliased ); break; case PktOid: whereClause = QStringLiteral( "%2oid=$%1" ).arg( offset ).arg( aliased ); break; case PktInt: case PktUint64: Q_ASSERT( mPrimaryKeyAttrs.size() == 1 ); whereClause = QStringLiteral( "%3%1=$%2" ).arg( quotedIdentifier( field( mPrimaryKeyAttrs[0] ).name() ) ).arg( offset ).arg( aliased ); break; case PktFidMap: { QString delim; for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ ) { int idx = mPrimaryKeyAttrs[i]; QgsField fld = field( idx ); whereClause += delim + QStringLiteral( "%3%1=$%2" ).arg( connectionRO()->fieldExpression( fld ) ).arg( offset++ ).arg( aliased ); delim = QStringLiteral( " AND " ); } } break; case PktUnknown: Q_ASSERT( !"FAILURE: Primary key unknown" ); whereClause = QStringLiteral( "NULL" ); break; } if ( !mSqlWhereClause.isEmpty() ) { if ( !whereClause.isEmpty() ) whereClause += QLatin1String( " AND " ); whereClause += '(' + mSqlWhereClause + ')'; } return whereClause; } void QgsPostgresProvider::appendPkParams( QgsFeatureId featureId, QStringList ¶ms ) const { switch ( mPrimaryKeyType ) { case PktOid: case PktUint64: params << QString::number( featureId ); break; case PktInt: params << QString::number( FID2PKINT( featureId ) ); break; case PktTid: params << QStringLiteral( "'(%1,%2)'" ).arg( FID_TO_NUMBER( featureId ) >> 16 ).arg( FID_TO_NUMBER( featureId ) & 0xffff ); break; case PktFidMap: { QVariantList pkVals = mShared->lookupKey( featureId ); if ( !pkVals.isEmpty() ) { Q_ASSERT( pkVals.size() == mPrimaryKeyAttrs.size() ); } for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ ) { if ( i < pkVals.size() ) { params << pkVals[i].toString(); } else { QgsDebugMsg( QStringLiteral( "FAILURE: Key value %1 for feature %2 not found." ).arg( mPrimaryKeyAttrs[i] ).arg( featureId ) ); params << QStringLiteral( "NULL" ); } } QgsDebugMsg( QStringLiteral( "keys params: %1" ).arg( params.join( "; " ) ) ); } break; case PktUnknown: Q_ASSERT( !"FAILURE: Primary key unknown" ); break; } } QString QgsPostgresProvider::whereClause( QgsFeatureId featureId ) const { return QgsPostgresUtils::whereClause( featureId, mAttributeFields, connectionRO(), mPrimaryKeyType, mPrimaryKeyAttrs, mShared ); } QString QgsPostgresUtils::whereClause( QgsFeatureId featureId, const QgsFields &fields, QgsPostgresConn *conn, QgsPostgresPrimaryKeyType pkType, const QList &pkAttrs, const std::shared_ptr &sharedData ) { QString whereClause; switch ( pkType ) { case PktTid: whereClause = QStringLiteral( "ctid='(%1,%2)'" ) .arg( FID_TO_NUMBER( featureId ) >> 16 ) .arg( FID_TO_NUMBER( featureId ) & 0xffff ); break; case PktOid: whereClause = QStringLiteral( "oid=%1" ).arg( featureId ); break; case PktInt: Q_ASSERT( pkAttrs.size() == 1 ); whereClause = QStringLiteral( "%1=%2" ).arg( QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) ).arg( FID2PKINT( featureId ) ); break; case PktUint64: Q_ASSERT( pkAttrs.size() == 1 ); whereClause = QStringLiteral( "%1=%2" ).arg( QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) ).arg( featureId ); break; case PktFidMap: { QVariantList pkVals = sharedData->lookupKey( featureId ); if ( !pkVals.isEmpty() ) { Q_ASSERT( pkVals.size() == pkAttrs.size() ); QString delim; for ( int i = 0; i < pkAttrs.size(); i++ ) { int idx = pkAttrs[i]; QgsField fld = fields.at( idx ); whereClause += delim + conn->fieldExpression( fld ); if ( pkVals[i].isNull() ) whereClause += QLatin1String( " IS NULL" ); else whereClause += '=' + QgsPostgresConn::quotedValue( pkVals[i].toString() ); delim = QStringLiteral( " AND " ); } } else { QgsDebugMsg( QStringLiteral( "FAILURE: Key values for feature %1 not found." ).arg( featureId ) ); whereClause = QStringLiteral( "NULL" ); } } break; case PktUnknown: Q_ASSERT( !"FAILURE: Primary key unknown" ); whereClause = QStringLiteral( "NULL" ); break; } return whereClause; } QString QgsPostgresUtils::whereClause( const QgsFeatureIds &featureIds, const QgsFields &fields, QgsPostgresConn *conn, QgsPostgresPrimaryKeyType pkType, const QList &pkAttrs, const std::shared_ptr &sharedData ) { switch ( pkType ) { case PktOid: case PktInt: case PktUint64: { QString expr; //simple primary key, so prefer to use an "IN (...)" query. These are much faster then multiple chained ...OR... clauses if ( !featureIds.isEmpty() ) { QString delim; expr = QStringLiteral( "%1 IN (" ).arg( ( pkType == PktOid ? QStringLiteral( "oid" ) : QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) ) ); Q_FOREACH ( const QgsFeatureId featureId, featureIds ) { expr += delim + FID_TO_STRING( ( pkType == PktOid ? featureId : pkType == PktUint64 ? featureId : FID2PKINT( featureId ) ) ); delim = ','; } expr += ')'; } return expr; } case PktFidMap: case PktTid: case PktUnknown: { //complex primary key, need to build up where string QStringList whereClauses; Q_FOREACH ( const QgsFeatureId featureId, featureIds ) { whereClauses << whereClause( featureId, fields, conn, pkType, pkAttrs, sharedData ); } return whereClauses.isEmpty() ? QString() : whereClauses.join( QStringLiteral( " OR " ) ).prepend( '(' ).append( ')' ); } } return QString(); //avoid warning } QString QgsPostgresUtils::andWhereClauses( const QString &c1, const QString &c2 ) { if ( c1.isEmpty() ) return c2; if ( c2.isEmpty() ) return c1; return QStringLiteral( "(%1) AND (%2)" ).arg( c1, c2 ); } QString QgsPostgresProvider::filterWhereClause() const { QString where; QString delim = QStringLiteral( " WHERE " ); if ( !mSqlWhereClause.isEmpty() ) { where += delim + '(' + mSqlWhereClause + ')'; delim = QStringLiteral( " AND " ); } if ( !mRequestedSrid.isEmpty() && ( mRequestedSrid != mDetectedSrid || mRequestedSrid.toInt() == 0 ) ) { where += delim + QStringLiteral( "%1(%2%3)=%4" ) .arg( connectionRO()->majorVersion() < 2 ? "srid" : "st_srid", quotedIdentifier( mGeometryColumn ), mSpatialColType == SctGeography ? "::geography" : "", mRequestedSrid ); delim = QStringLiteral( " AND " ); } if ( mRequestedGeomType != QgsWkbTypes::Unknown && mRequestedGeomType != mDetectedGeomType ) { where += delim + QgsPostgresConn::postgisTypeFilter( mGeometryColumn, ( QgsWkbTypes::Type )mRequestedGeomType, mSpatialColType == SctGeography ); delim = QStringLiteral( " AND " ); } return where; } void QgsPostgresProvider::setExtent( QgsRectangle &newExtent ) { mLayerExtent.setXMaximum( newExtent.xMaximum() ); mLayerExtent.setXMinimum( newExtent.xMinimum() ); mLayerExtent.setYMaximum( newExtent.yMaximum() ); mLayerExtent.setYMinimum( newExtent.yMinimum() ); } /** * Returns the feature type */ QgsWkbTypes::Type QgsPostgresProvider::wkbType() const { return mRequestedGeomType != QgsWkbTypes::Unknown ? mRequestedGeomType : mDetectedGeomType; } QgsLayerMetadata QgsPostgresProvider::layerMetadata() const { return mLayerMetadata; } QgsField QgsPostgresProvider::field( int index ) const { if ( index < 0 || index >= mAttributeFields.count() ) { QgsMessageLog::logMessage( tr( "FAILURE: Field %1 not found." ).arg( index ), tr( "PostGIS" ) ); throw PGFieldNotFound(); } return mAttributeFields.at( index ); } QgsFields QgsPostgresProvider::fields() const { return mAttributeFields; } QString QgsPostgresProvider::dataComment() const { return mDataComment; } //! \todo XXX Perhaps this should be promoted to QgsDataProvider? QString QgsPostgresProvider::endianString() { switch ( QgsApplication::endian() ) { case QgsApplication::NDR: return QStringLiteral( "NDR" ); case QgsApplication::XDR: return QStringLiteral( "XDR" ); default : return QStringLiteral( "Unknown" ); } } struct PGTypeInfo { QString typeName; QString typeType; QString typeElem; int typeLen; }; bool QgsPostgresProvider::loadFields() { if ( !mIsQuery ) { QgsDebugMsg( QStringLiteral( "Loading fields for table %1" ).arg( mTableName ) ); // Get the relation oid for use in later queries QString sql = QStringLiteral( "SELECT regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); QgsPostgresResult tresult( connectionRO()->PQexec( sql ) ); QString tableoid = tresult.PQgetvalue( 0, 0 ); // Get the table description sql = QStringLiteral( "SELECT description FROM pg_description WHERE objoid=%1 AND objsubid=0" ).arg( tableoid ); tresult = connectionRO()->PQexec( sql ); if ( tresult.PQntuples() > 0 ) { mDataComment = tresult.PQgetvalue( 0, 0 ); mLayerMetadata.setAbstract( mDataComment ); } } // Populate the field vector for this layer. The field vector contains // field name, type, length, and precision (if numeric) QString sql = QStringLiteral( "SELECT * FROM %1 LIMIT 0" ).arg( mQuery ); QgsPostgresResult result( connectionRO()->PQexec( sql ) ); // Collect type info sql = QStringLiteral( "SELECT oid,typname,typtype,typelem,typlen FROM pg_type" ); QgsPostgresResult typeResult( connectionRO()->PQexec( sql ) ); QMap typeMap; for ( int i = 0; i < typeResult.PQntuples(); ++i ) { PGTypeInfo typeInfo = { /* typeName = */ typeResult.PQgetvalue( i, 1 ), /* typeType = */ typeResult.PQgetvalue( i, 2 ), /* typeElem = */ typeResult.PQgetvalue( i, 3 ), /* typeLen = */ typeResult.PQgetvalue( i, 4 ).toInt() }; typeMap.insert( typeResult.PQgetvalue( i, 0 ).toInt(), typeInfo ); } QMap > fmtFieldTypeMap, descrMap, defValMap; QMap > attTypeIdMap; QMap > notNullMap, uniqueMap; if ( result.PQnfields() > 0 ) { // Collect table oids QSet tableoids; for ( int i = 0; i < result.PQnfields(); i++ ) { int tableoid = result.PQftable( i ); if ( tableoid > 0 ) { tableoids.insert( tableoid ); } } if ( !tableoids.isEmpty() ) { QStringList tableoidsList; Q_FOREACH ( int tableoid, tableoids ) { tableoidsList.append( QString::number( tableoid ) ); } QString tableoidsFilter = '(' + tableoidsList.join( QStringLiteral( "," ) ) + ')'; // Collect formatted field types sql = "SELECT attrelid, attnum, pg_catalog.format_type(atttypid,atttypmod), pg_catalog.col_description(attrelid,attnum), pg_catalog.pg_get_expr(adbin,adrelid), atttypid, attnotnull::int, indisunique::int" " FROM pg_attribute" " LEFT OUTER JOIN pg_attrdef ON attrelid=adrelid AND attnum=adnum" // find unique constraints if present. Text cast required to handle int2vector comparison. Distinct required as multiple unique constraints may exist " LEFT OUTER JOIN ( SELECT DISTINCT indrelid, indkey, indisunique FROM pg_index WHERE indisunique ) uniq ON attrelid=indrelid AND attnum::text=indkey::text " " WHERE attrelid IN " + tableoidsFilter; QgsPostgresResult fmtFieldTypeResult( connectionRO()->PQexec( sql ) ); for ( int i = 0; i < fmtFieldTypeResult.PQntuples(); ++i ) { int attrelid = fmtFieldTypeResult.PQgetvalue( i, 0 ).toInt(); int attnum = fmtFieldTypeResult.PQgetvalue( i, 1 ).toInt(); QString formatType = fmtFieldTypeResult.PQgetvalue( i, 2 ); QString descr = fmtFieldTypeResult.PQgetvalue( i, 3 ); QString defVal = fmtFieldTypeResult.PQgetvalue( i, 4 ); int attType = fmtFieldTypeResult.PQgetvalue( i, 5 ).toInt(); bool attNotNull = fmtFieldTypeResult.PQgetvalue( i, 6 ).toInt(); bool uniqueConstraint = fmtFieldTypeResult.PQgetvalue( i, 7 ).toInt(); fmtFieldTypeMap[attrelid][attnum] = formatType; descrMap[attrelid][attnum] = descr; defValMap[attrelid][attnum] = defVal; attTypeIdMap[attrelid][attnum] = attType; notNullMap[attrelid][attnum] = attNotNull; uniqueMap[attrelid][attnum] = uniqueConstraint; } } } QSet fields; mAttributeFields.clear(); for ( int i = 0; i < result.PQnfields(); i++ ) { QString fieldName = result.PQfname( i ); if ( fieldName == mGeometryColumn ) continue; int fldtyp = result.PQftype( i ); int fldMod = result.PQfmod( i ); int fieldPrec = -1; int tableoid = result.PQftable( i ); int attnum = result.PQftablecol( i ); int atttypid = attTypeIdMap[tableoid][attnum]; const PGTypeInfo &typeInfo = typeMap.value( fldtyp ); QString fieldTypeName = typeInfo.typeName; QString fieldTType = typeInfo.typeType; int fieldSize = typeInfo.typeLen; bool isDomain = ( typeMap.value( atttypid ).typeType == QLatin1String( "d" ) ); QString formattedFieldType = fmtFieldTypeMap[tableoid][attnum]; QString originalFormattedFieldType = formattedFieldType; if ( isDomain ) { // get correct formatted field type for domain sql = QStringLiteral( "SELECT format_type(%1, %2)" ).arg( fldtyp ).arg( fldMod ); QgsPostgresResult fmtFieldModResult( connectionRO()->PQexec( sql ) ); if ( fmtFieldModResult.PQntuples() > 0 ) { formattedFieldType = fmtFieldModResult.PQgetvalue( 0, 0 ); } } QString fieldComment = descrMap[tableoid][attnum]; QVariant::Type fieldType; QVariant::Type fieldSubType = QVariant::Invalid; if ( fieldTType == QLatin1String( "b" ) ) { bool isArray = fieldTypeName.startsWith( '_' ); if ( isArray ) fieldTypeName = fieldTypeName.mid( 1 ); if ( fieldTypeName == QLatin1String( "int8" ) || fieldTypeName == QLatin1String( "serial8" ) ) { fieldType = QVariant::LongLong; fieldSize = -1; fieldPrec = 0; } else if ( fieldTypeName == QLatin1String( "int2" ) || fieldTypeName == QLatin1String( "int4" ) || fieldTypeName == QLatin1String( "oid" ) || fieldTypeName == QLatin1String( "serial" ) ) { fieldType = QVariant::Int; fieldSize = -1; fieldPrec = 0; } else if ( fieldTypeName == QLatin1String( "real" ) || fieldTypeName == QLatin1String( "double precision" ) || fieldTypeName == QLatin1String( "float4" ) || fieldTypeName == QLatin1String( "float8" ) ) { fieldType = QVariant::Double; fieldSize = -1; fieldPrec = -1; } else if ( fieldTypeName == QLatin1String( "numeric" ) ) { fieldType = QVariant::Double; if ( formattedFieldType == QLatin1String( "numeric" ) || formattedFieldType.isEmpty() ) { 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 != QLatin1String( "numeric" ) ) { QgsMessageLog::logMessage( tr( "unexpected formatted field type '%1' for field %2" ) .arg( formattedFieldType, fieldName ), tr( "PostGIS" ) ); fieldSize = -1; fieldPrec = -1; } } } else if ( fieldTypeName == QLatin1String( "varchar" ) ) { fieldType = QVariant::String; QRegExp re( "character varying\\((\\d+)\\)" ); if ( re.exactMatch( formattedFieldType ) ) { fieldSize = re.cap( 1 ).toInt(); } else { fieldSize = -1; } } else if ( fieldTypeName == QLatin1String( "date" ) ) { fieldType = QVariant::Date; fieldSize = -1; } else if ( fieldTypeName == QLatin1String( "time" ) ) { fieldType = QVariant::Time; fieldSize = -1; } else if ( fieldTypeName == QLatin1String( "timestamp" ) ) { fieldType = QVariant::DateTime; fieldSize = -1; } else if ( fieldTypeName == QLatin1String( "text" ) || fieldTypeName == QLatin1String( "geometry" ) || fieldTypeName == QLatin1String( "inet" ) || fieldTypeName == QLatin1String( "money" ) || fieldTypeName == QLatin1String( "ltree" ) || fieldTypeName == QLatin1String( "uuid" ) || fieldTypeName == QLatin1String( "xml" ) || fieldTypeName.startsWith( QLatin1String( "time" ) ) || fieldTypeName.startsWith( QLatin1String( "date" ) ) ) { fieldType = QVariant::String; fieldSize = -1; } else if ( fieldTypeName == QLatin1String( "bpchar" ) ) { // although postgres internally uses "bpchar", this is exposed to users as character in postgres fieldTypeName = QStringLiteral( "character" ); fieldType = QVariant::String; QRegExp re( "character\\((\\d+)\\)" ); if ( re.exactMatch( formattedFieldType ) ) { fieldSize = re.cap( 1 ).toInt(); } else { QgsDebugMsg( QStringLiteral( "unexpected formatted field type '%1' for field %2" ) .arg( formattedFieldType, fieldName ) ); fieldSize = -1; fieldPrec = -1; } } else if ( fieldTypeName == QLatin1String( "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, fieldName ) ); fieldSize = -1; fieldPrec = -1; } } else if ( fieldTypeName == QLatin1String( "hstore" ) || fieldTypeName == QLatin1String( "json" ) || fieldTypeName == QLatin1String( "jsonb" ) ) { fieldType = QVariant::Map; fieldSubType = QVariant::String; fieldSize = -1; } else if ( fieldTypeName == QLatin1String( "bool" ) ) { // enum fieldType = QVariant::Bool; fieldSize = -1; } else { QgsMessageLog::logMessage( tr( "Field %1 ignored, because of unsupported type %2" ).arg( fieldName, fieldTypeName ), tr( "PostGIS" ) ); continue; } if ( isArray ) { fieldTypeName = '_' + fieldTypeName; fieldSubType = fieldType; fieldType = ( fieldType == QVariant::String ? QVariant::StringList : QVariant::List ); fieldSize = -1; } } else if ( fieldTType == QLatin1String( "e" ) ) { // enum fieldType = QVariant::String; fieldSize = -1; } else { QgsMessageLog::logMessage( tr( "Field %1 ignored, because of unsupported type %2" ).arg( fieldName, fieldTType ), tr( "PostGIS" ) ); continue; } if ( fields.contains( fieldName ) ) { QgsMessageLog::logMessage( tr( "Duplicate field %1 found\n" ).arg( fieldName ), tr( "PostGIS" ) ); return false; } fields << fieldName; if ( isDomain ) { //field was defined using domain, so use domain type name for fieldTypeName fieldTypeName = originalFormattedFieldType; } mAttrPalIndexName.insert( i, fieldName ); mDefaultValues.insert( mAttributeFields.size(), defValMap[tableoid][attnum] ); QgsField newField = QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment, fieldSubType ); QgsFieldConstraints constraints; if ( notNullMap[tableoid][attnum] || mPrimaryKeyAttrs.contains( i ) ) constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); if ( uniqueMap[tableoid][attnum] || mPrimaryKeyAttrs.contains( i ) ) constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); newField.setConstraints( constraints ); mAttributeFields.append( newField ); } setEditorWidgets(); return true; } void QgsPostgresProvider::setEditorWidgets() { if ( tableExists( *connectionRO(), EDITOR_WIDGET_STYLES_TABLE ) ) { for ( int i = 0; i < mAttributeFields.count(); ++i ) { // CREATE TABLE qgis_editor_widget_styles (schema_name TEXT NOT NULL, table_name TEXT NOT NULL, field_name TEXT NOT NULL, // type TEXT NOT NULL, config TEXT, // PRIMARY KEY(schema_name, table_name, field_name)); QgsField &field = mAttributeFields[i]; const QString sql = QStringLiteral( "SELECT type, config FROM %1 WHERE schema_name = %2 and table_name = %3 and field_name = %4 LIMIT 1" ). arg( EDITOR_WIDGET_STYLES_TABLE, quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( field.name() ) ); QgsPostgresResult result( connectionRO()->PQexec( sql ) ); for ( int i = 0; i < result.PQntuples(); ++i ) { const QString type = result.PQgetvalue( i, 0 ); QVariantMap config; if ( !result.PQgetisnull( i, 1 ) ) // Can be null and it's OK { const QString configTxt = result.PQgetvalue( i, 1 ); QDomDocument doc; if ( doc.setContent( configTxt ) ) { config = QgsXmlUtils::readVariant( doc.documentElement() ).toMap(); } else { QgsMessageLog::logMessage( tr( "Cannot parse widget configuration for field %1.%2.%3\n" ).arg( mSchemaName, mTableName, field.name() ), tr( "PostGIS" ) ); } } field.setEditorWidgetSetup( QgsEditorWidgetSetup( type, config ) ); } } } } bool QgsPostgresProvider::hasSufficientPermsAndCapabilities() { QgsDebugMsg( QStringLiteral( "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 = QStringLiteral( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery ); QgsPostgresResult testAccess( connectionRO()->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, testAccess.PQresultErrorMessage(), sql ), tr( "PostGIS" ) ); return false; } bool inRecovery = false; if ( connectionRO()->pgVersion() >= 90000 ) { testAccess = connectionRO()->PQexec( QStringLiteral( "SELECT pg_is_in_recovery()" ) ); if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK || testAccess.PQgetvalue( 0, 0 ) == QLatin1String( "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; } 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 ), mGeometryColumn.isNull() ? QStringLiteral( "'f'," ) : QStringLiteral( "has_column_privilege(%1,%2,'UPDATE')," ) .arg( quotedValue( mQuery ), 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 = connectionRO()->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, testAccess.PQresultErrorMessage(), sql ), tr( "PostGIS" ) ); return false; } if ( testAccess.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) ) { // DELETE mEnabledCapabilities |= QgsVectorDataProvider::DeleteFeatures | QgsVectorDataProvider::FastTruncate; } if ( testAccess.PQgetvalue( 0, 1 ) == QLatin1String( "t" ) ) { // UPDATE mEnabledCapabilities |= QgsVectorDataProvider::ChangeAttributeValues; } if ( testAccess.PQgetvalue( 0, 2 ) == QLatin1String( "t" ) ) { // UPDATE mEnabledCapabilities |= QgsVectorDataProvider::ChangeGeometries; } if ( testAccess.PQgetvalue( 0, 3 ) == QLatin1String( "t" ) ) { // INSERT mEnabledCapabilities |= QgsVectorDataProvider::AddFeatures; } if ( mSchemaName.isEmpty() ) mSchemaName = testAccess.PQgetvalue( 0, 4 ); sql = QString( "SELECT 1 FROM pg_class,pg_namespace WHERE " "pg_class.relnamespace=pg_namespace.oid AND " "%3 AND " "relname=%1 AND nspname=%2" ) .arg( quotedValue( mTableName ), quotedValue( mSchemaName ), connectionRO()->pgVersion() < 80100 ? "pg_get_userbyid(relowner)=current_user" : "pg_has_role(relowner,'MEMBER')" ); testAccess = connectionRO()->PQexec( sql ); if ( testAccess.PQresultStatus() == PGRES_TUPLES_OK && testAccess.PQntuples() == 1 ) { mEnabledCapabilities |= QgsVectorDataProvider::AddAttributes | QgsVectorDataProvider::DeleteAttributes | QgsVectorDataProvider::RenameAttributes; } } } else { // Check if the sql is a select query if ( !mQuery.startsWith( '(' ) && !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 = QStringLiteral( "subQuery_%1" ).arg( QString::number( index++ ) ); QString pattern = QStringLiteral( "(\\\"?)%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 = QStringLiteral( "%1 AS %2" ) .arg( mQuery, quotedIdentifier( alias ) ); QString sql = QStringLiteral( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery ); testAccess = connectionRO()->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(), sql ), tr( "PostGIS" ) ); return false; } if ( !mSelectAtIdDisabled ) { mEnabledCapabilities = QgsVectorDataProvider::SelectAtId; } } // supports geometry simplification on provider side mEnabledCapabilities |= ( QgsVectorDataProvider::SimplifyGeometries | QgsVectorDataProvider::SimplifyGeometriesWithTopologicalValidation ); //supports transactions mEnabledCapabilities |= QgsVectorDataProvider::TransactionSupport; // supports circular geometries mEnabledCapabilities |= QgsVectorDataProvider::CircularGeometries; // supports layer metadata mEnabledCapabilities |= QgsVectorDataProvider::ReadLayerMetadata; if ( ( mEnabledCapabilities & QgsVectorDataProvider::ChangeGeometries ) && ( mEnabledCapabilities & QgsVectorDataProvider::ChangeAttributeValues ) && mSpatialColType != SctTopoGeometry ) { mEnabledCapabilities |= QgsVectorDataProvider::ChangeFeatures; } return true; } bool QgsPostgresProvider::determinePrimaryKey() { 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 = QStringLiteral( "SELECT count(*) FROM pg_inherits WHERE inhparent=%1::regclass" ).arg( quotedValue( mQuery ) ); QgsDebugMsg( QStringLiteral( "Checking whether %1 is a parent table" ).arg( sql ) ); QgsPostgresResult res( connectionRO()->PQexec( sql ) ); bool isParentTable( res.PQntuples() == 0 || res.PQgetvalue( 0, 0 ).toInt() > 0 ); sql = QStringLiteral( "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( QStringLiteral( "Retrieving first primary or unique index: %1" ).arg( sql ) ); res = connectionRO()->PQexec( sql ); QgsDebugMsg( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ) ); QStringList log; // no primary or unique indizes found if ( res.PQntuples() == 0 ) { QgsDebugMsg( QStringLiteral( "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. QgsPostgresProvider::Relkind type = relkind(); if ( type == Relkind::OrdinaryTable || type == Relkind::PartitionedTable ) { QgsDebugMsg( QStringLiteral( "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 = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attname='oid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) ); res = connectionRO()->PQexec( sql ); if ( res.PQntuples() == 1 ) { // 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 = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attname='ctid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) ); res = connectionRO()->PQexec( sql ); if ( res.PQntuples() == 1 ) { mPrimaryKeyType = PktTid; QgsMessageLog::logMessage( tr( "Primary key is ctid - changing of existing features disabled (%1; %2)" ).arg( mGeometryColumn, mQuery ) ); mEnabledCapabilities &= ~( QgsVectorDataProvider::DeleteFeatures | QgsVectorDataProvider::ChangeAttributeValues | QgsVectorDataProvider::ChangeGeometries | QgsVectorDataProvider::ChangeFeatures ); } else { QgsMessageLog::logMessage( tr( "The table has no column suitable for use as a key. QGIS requires a primary key, a PostgreSQL oid column or a ctid for tables." ), tr( "PostGIS" ) ); } } } else if ( type == Relkind::View || type == Relkind::MaterializedView ) { determinePrimaryKeyFromUriKeyColumn(); } else { const QMetaEnum metaEnum( QMetaEnum::fromType() ); QString typeName = metaEnum.valueToKey( type ); QgsMessageLog::logMessage( tr( "Unexpected relation type '%1'." ).arg( typeName ), tr( "PostGIS" ) ); } } else { // have a primary key or unique index QString indrelid = res.PQgetvalue( 0, 0 ); sql = QStringLiteral( "SELECT attname,attnotnull 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 = connectionRO()->PQexec( sql ); QgsDebugMsg( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ) ); bool mightBeNull = false; QString primaryKey; QString delim; mPrimaryKeyType = PktFidMap; // map by default, will downgrade if needed for ( int i = 0; i < res.PQntuples(); i++ ) { QString name = res.PQgetvalue( i, 0 ); if ( res.PQgetvalue( i, 1 ).startsWith( 'f' ) ) { QgsMessageLog::logMessage( tr( "Unique column '%1' doesn't have a NOT NULL constraint." ).arg( name ), tr( "PostGIS" ) ); mightBeNull = true; } primaryKey += delim + quotedIdentifier( name ); delim = ','; int idx = fieldNameIndex( name ); if ( idx == -1 ) { QgsDebugMsg( "Skipping " + name ); continue; } QgsField fld = mAttributeFields.at( idx ); // Always use PktFidMap for multi-field keys mPrimaryKeyType = i ? PktFidMap : pkType( fld ); mPrimaryKeyAttrs << idx; } if ( ( mightBeNull || isParentTable ) && !mUseEstimatedMetadata && !uniqueData( primaryKey ) ) { QgsMessageLog::logMessage( tr( "Ignoring key candidate because of NULL values or inheritance" ), tr( "PostGIS" ) ); mPrimaryKeyType = PktUnknown; mPrimaryKeyAttrs.clear(); } } } else { determinePrimaryKeyFromUriKeyColumn(); } Q_FOREACH ( int fieldIdx, mPrimaryKeyAttrs ) { //primary keys are unique, not null QgsFieldConstraints constraints = mAttributeFields.at( fieldIdx ).constraints(); constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); mAttributeFields[ fieldIdx ].setConstraints( constraints ); } mValid = mPrimaryKeyType != PktUnknown; return mValid; } /* static */ QStringList QgsPostgresProvider::parseUriKey( const QString &key ) { if ( key.isEmpty() ) return QStringList(); QStringList cols; // remove quotes from key list if ( key.startsWith( '"' ) && key.endsWith( '"' ) ) { int i = 1; QString col; while ( i < key.size() ) { if ( key[i] == '"' ) { if ( i + 1 < key.size() && key[i + 1] == '"' ) { i++; } else { cols << col; col.clear(); if ( ++i == key.size() ) break; Q_ASSERT( key[i] == ',' ); i++; Q_ASSERT( key[i] == '"' ); i++; col.clear(); continue; } } col += key[i++]; } } else if ( key.contains( ',' ) ) { cols = key.split( ',' ); } else { cols << key; } return cols; } void QgsPostgresProvider::determinePrimaryKeyFromUriKeyColumn() { QString primaryKey = mUri.keyColumn(); mPrimaryKeyType = PktUnknown; if ( !primaryKey.isEmpty() ) { QStringList cols = parseUriKey( primaryKey ); primaryKey.clear(); QString del; Q_FOREACH ( const QString &col, cols ) { primaryKey += del + quotedIdentifier( col ); del = QStringLiteral( "," ); } Q_FOREACH ( const QString &col, cols ) { int idx = fieldNameIndex( col ); if ( idx < 0 ) { QgsMessageLog::logMessage( tr( "Key field '%1' for view/query not found." ).arg( col ), tr( "PostGIS" ) ); mPrimaryKeyAttrs.clear(); break; } mPrimaryKeyAttrs << idx; } if ( !mPrimaryKeyAttrs.isEmpty() ) { bool unique = true; if ( mCheckPrimaryKeyUnicity ) { unique = uniqueData( primaryKey ); } if ( mUseEstimatedMetadata || unique ) { mPrimaryKeyType = PktFidMap; // Map by default if ( mPrimaryKeyAttrs.size() == 1 ) { QgsField fld = mAttributeFields.at( 0 ); mPrimaryKeyType = pkType( fld ); } } else { QgsMessageLog::logMessage( tr( "Primary key field '%1' for view/query not unique." ).arg( primaryKey ), tr( "PostGIS" ) ); } } else { QgsMessageLog::logMessage( tr( "Keys for view/query undefined." ), tr( "PostGIS" ) ); } } else { QgsMessageLog::logMessage( tr( "No key field for view/query given." ), tr( "PostGIS" ) ); } } bool QgsPostgresProvider::uniqueData( const QString "edColNames ) { // Check to see if the given columns contain unique data QString sql = QStringLiteral( "SELECT count(distinct (%1))=count((%1)) FROM %2%3" ) .arg( quotedColNames, mQuery, filterWhereClause() ); QgsPostgresResult unique( connectionRO()->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 ) const { try { // get the field name QgsField fld = field( index ); QString sql = QStringLiteral( "SELECT min(%1) AS %1 FROM %2" ) .arg( quotedIdentifier( fld.name() ), mQuery ); if ( !mSqlWhereClause.isEmpty() ) { sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause ); } sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql ); QgsPostgresResult rmin( connectionRO()->PQexec( sql ) ); return convertValue( fld.type(), fld.subType(), rmin.PQgetvalue( 0, 0 ), fld.typeName() ); } catch ( PGFieldNotFound ) { return QVariant( QString() ); } } // Returns the list of unique values of an attribute QSet QgsPostgresProvider::uniqueValues( int index, int limit ) const { QSet uniqueValues; try { // get the field name QgsField fld = field( index ); QString sql = QStringLiteral( "SELECT DISTINCT %1 FROM %2" ) .arg( quotedIdentifier( fld.name() ), mQuery ); if ( !mSqlWhereClause.isEmpty() ) { sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause ); } sql += QStringLiteral( " ORDER BY %1" ).arg( quotedIdentifier( fld.name() ) ); if ( limit >= 0 ) { sql += QStringLiteral( " LIMIT %1" ).arg( limit ); } sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql ); QgsPostgresResult res( connectionRO()->PQexec( sql ) ); if ( res.PQresultStatus() == PGRES_TUPLES_OK ) { for ( int i = 0; i < res.PQntuples(); i++ ) uniqueValues.insert( convertValue( fld.type(), fld.subType(), res.PQgetvalue( i, 0 ), fld.typeName() ) ); } } catch ( PGFieldNotFound ) { } return uniqueValues; } QStringList QgsPostgresProvider::uniqueStringsMatching( int index, const QString &substring, int limit, QgsFeedback *feedback ) const { QStringList results; try { // get the field name QgsField fld = field( index ); QString sql = QStringLiteral( "SELECT DISTINCT %1 FROM %2 WHERE" ) .arg( quotedIdentifier( fld.name() ), mQuery ); if ( !mSqlWhereClause.isEmpty() ) { sql += QStringLiteral( " ( %1 ) AND " ).arg( mSqlWhereClause ); } sql += QStringLiteral( " %1::text ILIKE '%%2%'" ).arg( quotedIdentifier( fld.name() ), substring ); sql += QStringLiteral( " ORDER BY %1" ).arg( quotedIdentifier( fld.name() ) ); if ( limit >= 0 ) { sql += QStringLiteral( " LIMIT %1" ).arg( limit ); } sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql ); QgsPostgresResult res( connectionRO()->PQexec( sql ) ); if ( res.PQresultStatus() == PGRES_TUPLES_OK ) { for ( int i = 0; i < res.PQntuples(); i++ ) { results << ( convertValue( fld.type(), fld.subType(), res.PQgetvalue( i, 0 ), fld.typeName() ) ).toString(); if ( feedback && feedback->isCanceled() ) break; } } } catch ( PGFieldNotFound ) { } return results; } void QgsPostgresProvider::enumValues( int index, QStringList &enumList ) const { enumList.clear(); if ( index < 0 || index >= mAttributeFields.count() ) return; //find out type of index QString fieldName = mAttributeFields.at( index ).name(); QString typeName = mAttributeFields.at( index ).typeName(); // Remove schema extension from typeName typeName.remove( QRegularExpression( "^([^.]+\\.)+" ) ); //is type an enum? QString typeSql = QStringLiteral( "SELECT typtype FROM pg_type WHERE typname=%1" ).arg( quotedValue( typeName ) ); QgsPostgresResult typeRes( connectionRO()->PQexec( typeSql ) ); if ( typeRes.PQresultStatus() != PGRES_TUPLES_OK || typeRes.PQntuples() < 1 ) { return; } QString typtype = typeRes.PQgetvalue( 0, 0 ); if ( typtype.compare( QLatin1String( "e" ), Qt::CaseInsensitive ) == 0 ) { //try to read enum_range of attribute if ( !parseEnumRange( enumList, fieldName ) ) { enumList.clear(); } } else { //is there a domain check constraint for the attribute? if ( !parseDomainCheckConstraint( enumList, fieldName ) ) { enumList.clear(); } } } bool QgsPostgresProvider::parseEnumRange( QStringList &enumValues, const QString &attributeName ) const { enumValues.clear(); QString enumRangeSql = QStringLiteral( "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 ), quotedValue( attributeName ) ); QgsPostgresResult enumRangeRes( connectionRO()->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 = QStringLiteral( "SELECT domain_name, domain_schema FROM information_schema.columns WHERE table_name=%1 AND column_name=%2" ).arg( quotedValue( mTableName ), quotedValue( attributeName ) ); QgsPostgresResult domainResult( connectionRO()->PQexec( domainSql ) ); if ( domainResult.PQresultStatus() == PGRES_TUPLES_OK && domainResult.PQntuples() > 0 && !domainResult.PQgetvalue( 0, 0 ).isNull() ) { //a domain type QString domainCheckDefinitionSql = QStringLiteral( "" "SELECT consrc FROM pg_constraint " " WHERE contypid =(" " SELECT oid FROM pg_type " " WHERE typname = %1 " " AND typnamespace =(" " SELECT oid FROM pg_namespace WHERE nspname = %2" " )" " )" ) .arg( quotedValue( domainResult.PQgetvalue( 0, 0 ) ) ) .arg( quotedValue( domainResult.PQgetvalue( 0, 1 ) ) ); QgsPostgresResult domainCheckRes( connectionRO()->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( QLatin1String( "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 ) const { try { // get the field name QgsField fld = field( index ); QString sql = QStringLiteral( "SELECT max(%1) AS %1 FROM %2" ) .arg( quotedIdentifier( fld.name() ), mQuery ); if ( !mSqlWhereClause.isEmpty() ) { sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause ); } sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql ); QgsPostgresResult rmax( connectionRO()->PQexec( sql ) ); return convertValue( fld.type(), fld.subType(), rmax.PQgetvalue( 0, 0 ), fld.typeName() ); } catch ( PGFieldNotFound ) { return QVariant( QString() ); } } bool QgsPostgresProvider::isValid() const { return mValid; } QString QgsPostgresProvider::defaultValueClause( int fieldId ) const { QString defVal = mDefaultValues.value( fieldId, QString() ); if ( !providerProperty( EvaluateDefaultValues, false ).toBool() && !defVal.isEmpty() ) { return defVal; } return QString(); } QVariant QgsPostgresProvider::defaultValue( int fieldId ) const { QString defVal = mDefaultValues.value( fieldId, QString() ); if ( providerProperty( EvaluateDefaultValues, false ).toBool() && !defVal.isEmpty() ) { QgsField fld = field( fieldId ); QgsPostgresResult res( connectionRO()->PQexec( QStringLiteral( "SELECT %1" ).arg( defVal ) ) ); if ( res.result() ) return convertValue( fld.type(), fld.subType(), res.PQgetvalue( 0, 0 ), fld.typeName() ); else { pushError( tr( "Could not execute query" ) ); return QVariant(); } } return QVariant(); } bool QgsPostgresProvider::skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint, const QVariant &value ) const { if ( providerProperty( EvaluateDefaultValues, false ).toBool() ) { return !mDefaultValues.value( fieldIndex ).isEmpty(); } else { // stricter check - if we are evaluating default values only on commit then we can only bypass the check // if the attribute values matches the original default clause return mDefaultValues.contains( fieldIndex ) && mDefaultValues.value( fieldIndex ) == value.toString() && !value.isNull(); } } QString QgsPostgresProvider::paramValue( const QString &fieldValue, const QString &defaultValue ) const { if ( fieldValue.isNull() ) return QString(); if ( fieldValue == defaultValue && !defaultValue.isNull() ) { QgsPostgresResult result( connectionRO()->PQexec( QStringLiteral( "SELECT %1" ).arg( defaultValue ) ) ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); return result.PQgetvalue( 0, 0 ); } return fieldValue; } /* private */ bool QgsPostgresProvider::getTopoLayerInfo() { QString sql = QString( "SELECT t.name, l.layer_id " "FROM topology.layer l, topology.topology t " "WHERE l.topology_id = t.id AND l.schema_name=%1 " "AND l.table_name=%2 AND l.feature_column=%3" ) .arg( quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( mGeometryColumn ) ); QgsPostgresResult result( connectionRO()->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { throw PGException( result ); // we should probably not do this } if ( result.PQntuples() < 1 ) { QgsMessageLog::logMessage( tr( "Could not find topology of layer %1.%2.%3" ) .arg( quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( mGeometryColumn ) ), tr( "PostGIS" ) ); return false; } mTopoLayerInfo.topologyName = result.PQgetvalue( 0, 0 ); mTopoLayerInfo.layerId = result.PQgetvalue( 0, 1 ).toLong(); return true; } /* private */ void QgsPostgresProvider::dropOrphanedTopoGeoms() { QString sql = QString( "DELETE FROM %1.relation WHERE layer_id = %2 AND " "topogeo_id NOT IN ( SELECT id(%3) FROM %4.%5 )" ) .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) ) .arg( mTopoLayerInfo.layerId ) .arg( quotedIdentifier( mGeometryColumn ), quotedIdentifier( mSchemaName ), quotedIdentifier( mTableName ) ) ; QgsDebugMsg( "TopoGeom orphans cleanup query: " + sql ); connectionRW()->PQexecNR( sql ); } QString QgsPostgresProvider::geomParam( int offset ) const { QString geometry; bool forceMulti = false; if ( mSpatialColType != SctTopoGeometry ) { forceMulti = QgsWkbTypes::isMultiType( wkbType() ); } if ( mSpatialColType == SctTopoGeometry ) { geometry += QStringLiteral( "toTopoGeom(" ); } if ( forceMulti ) { geometry += connectionRO()->majorVersion() < 2 ? "multi(" : "st_multi("; } geometry += QStringLiteral( "%1($%2%3,%4)" ) .arg( connectionRO()->majorVersion() < 2 ? "geomfromwkb" : "st_geomfromwkb" ) .arg( offset ) .arg( connectionRO()->useWkbHex() ? "" : "::bytea", mRequestedSrid.isEmpty() ? mDetectedSrid : mRequestedSrid ); if ( forceMulti ) { geometry += ')'; } if ( mSpatialColType == SctTopoGeometry ) { geometry += QStringLiteral( ",%1,%2)" ) .arg( quotedValue( mTopoLayerInfo.topologyName ) ) .arg( mTopoLayerInfo.layerId ); } return geometry; } bool QgsPostgresProvider::addFeatures( QgsFeatureList &flist, Flags flags ) { if ( flist.isEmpty() ) return true; if ( mIsQuery ) return false; QgsPostgresConn *conn = connectionRW(); if ( !conn ) { return false; } conn->lock(); bool returnvalue = true; try { conn->begin(); // Prepare the INSERT statement QString insert = QStringLiteral( "INSERT INTO %1(" ).arg( mQuery ); QString values = QStringLiteral( ") VALUES (" ); QString delim; int offset = 1; QStringList defaultValues; QList fieldId; if ( !mGeometryColumn.isNull() ) { insert += quotedIdentifier( mGeometryColumn ); values += geomParam( offset++ ); delim = ','; } // Optimization: if we have a single primary key column whose default value // is a sequence, and that none of the features have a value set for that // column, then we can completely omit inserting it. bool skipSinglePKField = false; if ( ( mPrimaryKeyType == PktInt || mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktUint64 ) ) { if ( mPrimaryKeyAttrs.size() == 1 && defaultValueClause( mPrimaryKeyAttrs[0] ).startsWith( "nextval(" ) ) { bool foundNonNullPK = false; int idx = mPrimaryKeyAttrs[0]; for ( int i = 0; i < flist.size(); i++ ) { QgsAttributes attrs2 = flist[i].attributes(); QVariant v2 = attrs2.value( idx, QVariant( QVariant::Int ) ); if ( !v2.isNull() ) { foundNonNullPK = true; break; } } skipSinglePKField = !foundNonNullPK; } if ( !skipSinglePKField ) { for ( int idx : mPrimaryKeyAttrs ) { insert += delim + quotedIdentifier( field( idx ).name() ); values += delim + QStringLiteral( "$%1" ).arg( defaultValues.size() + offset ); delim = ','; fieldId << idx; defaultValues << defaultValueClause( idx ); } } } QgsAttributes attributevec = flist[0].attributes(); // look for unique attribute values to place in statement instead of passing as parameter // e.g. for defaults for ( int idx = 0; idx < attributevec.count(); ++idx ) { QVariant v = attributevec.value( idx, QVariant( QVariant::Int ) ); // default to NULL for missing attributes if ( skipSinglePKField && idx == mPrimaryKeyAttrs[0] ) continue; if ( fieldId.contains( idx ) ) continue; if ( idx >= mAttributeFields.count() ) continue; QString fieldname = mAttributeFields.at( idx ).name(); QString fieldTypeName = mAttributeFields.at( idx ).typeName(); QgsDebugMsg( "Checking field against: " + fieldname ); if ( fieldname.isEmpty() || fieldname == mGeometryColumn ) continue; int i; for ( i = 1; i < flist.size(); i++ ) { QgsAttributes attrs2 = flist[i].attributes(); QVariant v2 = attrs2.value( idx, QVariant( QVariant::Int ) ); // default to NULL for missing attributes if ( v2 != v ) break; } insert += delim + quotedIdentifier( fieldname ); QString defVal = defaultValueClause( idx ); if ( i == flist.size() ) { if ( qgsVariantEqual( v, defVal ) ) { if ( defVal.isNull() ) { values += delim + "NULL"; } else { values += delim + defVal; } } else if ( fieldTypeName == QLatin1String( "geometry" ) ) { values += QStringLiteral( "%1%2(%3)" ) .arg( delim, connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( v.toString() ) ); } else if ( fieldTypeName == QLatin1String( "geography" ) ) { values += QStringLiteral( "%1st_geographyfromewkt(%2)" ) .arg( delim, quotedValue( v.toString() ) ); } //TODO: convert arrays and hstore to native types else { //this should be for json/jsonb in future values += delim + quotedValue( v ); } } else { // value is not unique => add parameter if ( fieldTypeName == QLatin1String( "geometry" ) ) { values += QStringLiteral( "%1%2($%3)" ) .arg( delim, connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" ) .arg( defaultValues.size() + offset ); } else if ( fieldTypeName == QLatin1String( "geography" ) ) { values += QStringLiteral( "%1st_geographyfromewkt($%2)" ) .arg( delim ) .arg( defaultValues.size() + offset ); } else { values += QStringLiteral( "%1$%2" ) .arg( delim ) .arg( defaultValues.size() + offset ); } defaultValues.append( defVal ); fieldId.append( idx ); } delim = ','; } insert += values + ')'; if ( !( flags & QgsFeatureSink::FastInsert ) ) { if ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt || mPrimaryKeyType == PktUint64 ) { insert += QLatin1String( " RETURNING " ); QString delim; Q_FOREACH ( int idx, mPrimaryKeyAttrs ) { insert += delim + quotedIdentifier( mAttributeFields.at( idx ).name() ); delim = ','; } } } QgsDebugMsg( QStringLiteral( "prepare addfeatures: %1" ).arg( insert ) ); QgsPostgresResult stmt( conn->PQprepare( QStringLiteral( "addfeatures" ), insert, fieldId.size() + offset - 1, nullptr ) ); if ( stmt.PQresultStatus() != PGRES_COMMAND_OK ) throw PGException( stmt ); for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); ++features ) { QgsAttributes attrs = features->attributes(); QStringList params; if ( !mGeometryColumn.isNull() ) { appendGeomParam( features->geometry(), params ); } params.reserve( fieldId.size() ); for ( int i = 0; i < fieldId.size(); i++ ) { int attrIdx = fieldId[i]; QVariant value = attrIdx < attrs.length() ? attrs.at( attrIdx ) : QVariant( QVariant::Int ); QString v; if ( value.isNull() ) { QgsField fld = field( attrIdx ); v = paramValue( defaultValues[ i ], defaultValues[ i ] ); features->setAttribute( attrIdx, convertValue( fld.type(), fld.subType(), v, fld.typeName() ) ); } else { v = paramValue( value.toString(), defaultValues[ i ] ); if ( v != value.toString() ) { QgsField fld = field( attrIdx ); features->setAttribute( attrIdx, convertValue( fld.type(), fld.subType(), v, fld.typeName() ) ); } } params << v; } QgsPostgresResult result( conn->PQexecPrepared( QStringLiteral( "addfeatures" ), params ) ); if ( !( flags & QgsFeatureSink::FastInsert ) && result.PQresultStatus() == PGRES_TUPLES_OK ) { for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i ) { const int idx = mPrimaryKeyAttrs.at( i ); const QgsField fld = mAttributeFields.at( idx ); features->setAttribute( idx, convertValue( fld.type(), fld.subType(), result.PQgetvalue( 0, i ), fld.typeName() ) ); } } else if ( result.PQresultStatus() != PGRES_COMMAND_OK ) throw PGException( result ); if ( !( flags & QgsFeatureSink::FastInsert ) && mPrimaryKeyType == PktOid ) { features->setId( result.PQoidValue() ); QgsDebugMsgLevel( QStringLiteral( "new fid=%1" ).arg( features->id() ), 4 ); } } if ( !( flags & QgsFeatureSink::FastInsert ) ) { // update feature ids if ( mPrimaryKeyType == PktInt || mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktUint64 ) { for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); ++features ) { QgsAttributes attrs = features->attributes(); if ( mPrimaryKeyType == PktUint64 ) { features->setId( STRING_TO_FID( attrs.at( mPrimaryKeyAttrs.at( 0 ) ) ) ); } else if ( mPrimaryKeyType == PktInt ) { features->setId( PKINT2FID( STRING_TO_FID( attrs.at( mPrimaryKeyAttrs.at( 0 ) ) ) ) ); } else { QVariantList primaryKeyVals; Q_FOREACH ( int idx, mPrimaryKeyAttrs ) { primaryKeyVals << attrs.at( idx ); } features->setId( mShared->lookupFid( primaryKeyVals ) ); } QgsDebugMsgLevel( QStringLiteral( "new fid=%1" ).arg( features->id() ), 4 ); } } } conn->PQexecNR( QStringLiteral( "DEALLOCATE addfeatures" ) ); returnvalue &= conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); mShared->addFeaturesCounted( flist.size() ); } catch ( PGException &e ) { pushError( tr( "PostGIS error while adding features: %1" ).arg( e.errorMessage() ) ); conn->rollback(); conn->PQexecNR( QStringLiteral( "DEALLOCATE addfeatures" ) ); returnvalue = false; } conn->unlock(); return returnvalue; } bool QgsPostgresProvider::deleteFeatures( const QgsFeatureIds &id ) { bool returnvalue = true; if ( mIsQuery ) { QgsDebugMsg( QStringLiteral( "Cannot delete features (is a query)" ) ); return false; } QgsPostgresConn *conn = connectionRW(); if ( !conn ) { return false; } conn->lock(); try { conn->begin(); for ( QgsFeatureIds::const_iterator it = id.begin(); it != id.end(); ++it ) { QString sql = QStringLiteral( "DELETE FROM %1 WHERE %2" ) .arg( mQuery, whereClause( *it ) ); QgsDebugMsg( "delete sql: " + sql ); //send DELETE statement and do error handling QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); mShared->removeFid( *it ); } returnvalue &= conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); if ( mSpatialColType == SctTopoGeometry ) { // NOTE: in presence of multiple TopoGeometry objects // for the same table or when deleting a Geometry // layer _also_ having a TopoGeometry component, // orphans would still be left. // TODO: decouple layer from table and signal table when // records are added or removed dropOrphanedTopoGeoms(); } mShared->addFeaturesCounted( -id.size() ); } catch ( PGException &e ) { pushError( tr( "PostGIS error while deleting features: %1" ).arg( e.errorMessage() ) ); conn->rollback(); returnvalue = false; } conn->unlock(); return returnvalue; } bool QgsPostgresProvider::truncate() { bool returnvalue = true; if ( mIsQuery ) { QgsDebugMsg( QStringLiteral( "Cannot truncate (is a query)" ) ); return false; } QgsPostgresConn *conn = connectionRW(); if ( !conn ) { return false; } conn->lock(); try { conn->begin(); QString sql = QStringLiteral( "TRUNCATE %1" ).arg( mQuery ); QgsDebugMsg( "truncate sql: " + sql ); //send truncate statement and do error handling QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); returnvalue &= conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); if ( returnvalue ) { if ( mSpatialColType == SctTopoGeometry ) { // NOTE: in presence of multiple TopoGeometry objects // for the same table or when deleting a Geometry // layer _also_ having a TopoGeometry component, // orphans would still be left. // TODO: decouple layer from table and signal table when // records are added or removed dropOrphanedTopoGeoms(); } mShared->clear(); } } catch ( PGException &e ) { pushError( tr( "PostGIS error while truncating: %1" ).arg( e.errorMessage() ) ); conn->rollback(); returnvalue = false; } conn->unlock(); return returnvalue; } bool QgsPostgresProvider::addAttributes( const QList &attributes ) { bool returnvalue = true; if ( mIsQuery ) return false; if ( attributes.isEmpty() ) return true; QgsPostgresConn *conn = connectionRW(); if ( !conn ) { return false; } conn->lock(); try { conn->begin(); QString delim; QString sql = QStringLiteral( "ALTER TABLE %1 " ).arg( mQuery ); for ( QList::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter ) { QString type = iter->typeName(); if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) ) { if ( iter->length() > 0 ) type = QStringLiteral( "%1(%2)" ).arg( type ).arg( iter->length() ); } else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) ) { if ( iter->length() > 0 && iter->precision() >= 0 ) type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( iter->length() ).arg( iter->precision() ); } sql.append( QStringLiteral( "%1ADD COLUMN %2 %3" ).arg( delim, quotedIdentifier( iter->name() ), type ) ); delim = ','; } //send sql statement and do error handling QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) throw PGException( result ); for ( QList::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter ) { if ( !iter->comment().isEmpty() ) { sql = QStringLiteral( "COMMENT ON COLUMN %1.%2 IS %3" ) .arg( mQuery, quotedIdentifier( iter->name() ), quotedValue( iter->comment() ) ); result = conn->PQexec( sql ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) throw PGException( result ); } } returnvalue &= conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); } catch ( PGException &e ) { pushError( tr( "PostGIS error while adding attributes: %1" ).arg( e.errorMessage() ) ); conn->rollback(); returnvalue = false; } loadFields(); conn->unlock(); return returnvalue; } bool QgsPostgresProvider::deleteAttributes( const QgsAttributeIds &ids ) { bool returnvalue = true; if ( mIsQuery ) return false; QgsPostgresConn *conn = connectionRW(); if ( !conn ) { return false; } conn->lock(); try { conn->begin(); QList idsList = ids.values(); std::sort( idsList.begin(), idsList.end(), std::greater() ); for ( auto iter = idsList.constBegin(); iter != idsList.constEnd(); ++iter ) { int index = *iter; if ( index < 0 || index >= mAttributeFields.count() ) continue; QString column = mAttributeFields.at( index ).name(); QString sql = QStringLiteral( "ALTER TABLE %1 DROP COLUMN %2" ) .arg( mQuery, quotedIdentifier( column ) ); //send sql statement and do error handling QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) throw PGException( result ); //delete the attribute from mAttributeFields mAttributeFields.remove( index ); } returnvalue &= conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); } catch ( PGException &e ) { pushError( tr( "PostGIS error while deleting attributes: %1" ).arg( e.errorMessage() ) ); conn->rollback(); returnvalue = false; } loadFields(); conn->unlock(); return returnvalue; } bool QgsPostgresProvider::renameAttributes( const QgsFieldNameMap &renamedAttributes ) { if ( mIsQuery ) return false; QString sql = QStringLiteral( "BEGIN;" ); QgsFieldNameMap::const_iterator renameIt = renamedAttributes.constBegin(); bool returnvalue = true; for ( ; renameIt != renamedAttributes.constEnd(); ++renameIt ) { int fieldIndex = renameIt.key(); if ( fieldIndex < 0 || fieldIndex >= mAttributeFields.count() ) { pushError( tr( "Invalid attribute index: %1" ).arg( fieldIndex ) ); return false; } if ( mAttributeFields.indexFromName( renameIt.value() ) >= 0 ) { //field name already in use pushError( tr( "Error renaming field %1: name '%2' already exists" ).arg( fieldIndex ).arg( renameIt.value() ) ); return false; } sql += QStringLiteral( "ALTER TABLE %1 RENAME COLUMN %2 TO %3;" ) .arg( mQuery, quotedIdentifier( mAttributeFields.at( fieldIndex ).name() ), quotedIdentifier( renameIt.value() ) ); } sql += QLatin1String( "COMMIT;" ); QgsPostgresConn *conn = connectionRW(); if ( !conn ) { return false; } conn->lock(); try { conn->begin(); //send sql statement and do error handling QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) throw PGException( result ); returnvalue = conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); } catch ( PGException &e ) { pushError( tr( "PostGIS error while renaming attributes: %1" ).arg( e.errorMessage() ) ); conn->rollback(); returnvalue = false; } loadFields(); conn->unlock(); return returnvalue; } bool QgsPostgresProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map ) { bool returnvalue = true; if ( mIsQuery ) return false; if ( attr_map.isEmpty() ) return true; QgsPostgresConn *conn = connectionRW(); if ( !conn ) return false; conn->lock(); try { conn->begin(); // cycle through the features for ( QgsChangedAttributesMap::const_iterator iter = attr_map.constBegin(); iter != attr_map.constEnd(); ++iter ) { QgsFeatureId fid = iter.key(); // skip added features if ( FID_IS_NEW( fid ) ) continue; const QgsAttributeMap &attrs = iter.value(); if ( attrs.isEmpty() ) continue; QString sql = QStringLiteral( "UPDATE %1 SET " ).arg( mQuery ); bool pkChanged = false; // cycle through the changed attributes of the feature QString delim; for ( QgsAttributeMap::const_iterator siter = attrs.constBegin(); siter != attrs.constEnd(); ++siter ) { try { QgsField fld = field( siter.key() ); pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() ); sql += delim + QStringLiteral( "%1=" ).arg( quotedIdentifier( fld.name() ) ); delim = ','; if ( fld.typeName() == QLatin1String( "geometry" ) ) { sql += QStringLiteral( "%1(%2)" ) .arg( connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( siter->toString() ) ); } else if ( fld.typeName() == QLatin1String( "geography" ) ) { sql += QStringLiteral( "st_geographyfromewkt(%1)" ) .arg( quotedValue( siter->toString() ) ); } else { sql += quotedValue( *siter ); } } catch ( PGFieldNotFound ) { // Field was missing - shouldn't happen } } sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); // update feature id map if key was changed if ( pkChanged && mPrimaryKeyType == PktFidMap ) { QVariantList k = mShared->removeFid( fid ); for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ ) { int idx = mPrimaryKeyAttrs.at( i ); if ( !attrs.contains( idx ) ) continue; k[i] = attrs[ idx ]; } mShared->insertFid( fid, k ); } } returnvalue &= conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); } catch ( PGException &e ) { pushError( tr( "PostGIS error while changing attributes: %1" ).arg( e.errorMessage() ) ); conn->rollback(); returnvalue = false; } conn->unlock(); return returnvalue; } void QgsPostgresProvider::appendGeomParam( const QgsGeometry &geom, QStringList ¶ms ) const { if ( geom.isNull() ) { params << QString(); return; } QString param; QgsGeometry convertedGeom( convertToProviderType( geom ) ); QByteArray wkb( !convertedGeom.isNull() ? convertedGeom.asWkb() : geom.asWkb() ); const unsigned char *buf = reinterpret_cast< const unsigned char * >( wkb.constData() ); int wkbSize = wkb.length(); for ( int i = 0; i < wkbSize; ++i ) { if ( connectionRO()->useWkbHex() ) param += QStringLiteral( "%1" ).arg( ( int ) buf[i], 2, 16, QChar( '0' ) ); else param += QStringLiteral( "\\%1" ).arg( ( int ) buf[i], 3, 8, QChar( '0' ) ); } params << param; } bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_map ) { if ( mIsQuery || mGeometryColumn.isNull() ) return false; QgsPostgresConn *conn = connectionRW(); if ( !conn ) { return false; } conn->lock(); bool returnvalue = true; try { // Start the PostGIS transaction conn->begin(); QString update; QgsPostgresResult result; if ( mSpatialColType == SctTopoGeometry ) { // We will create a new TopoGeometry object with the new shape. // Later, we'll replace the old TopoGeometry with the new one, // to avoid orphans and retain higher level in an eventual // hierarchical definition update = QStringLiteral( "SELECT id(%1) FROM %2 o WHERE %3" ) .arg( geomParam( 1 ), mQuery, pkParamWhereClause( 2 ) ); QString getid = QStringLiteral( "SELECT id(%1) FROM %2 WHERE %3" ) .arg( quotedIdentifier( mGeometryColumn ), mQuery, pkParamWhereClause( 1 ) ); QgsDebugMsg( "getting old topogeometry id: " + getid ); result = connectionRO()->PQprepare( QStringLiteral( "getid" ), getid, 1, nullptr ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { QgsDebugMsg( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" ) .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( getid ) ); throw PGException( result ); } QString replace = QString( "UPDATE %1 SET %2=" "( topology_id(%2),layer_id(%2),$1,type(%2) )" "WHERE %3" ) .arg( mQuery, quotedIdentifier( mGeometryColumn ), pkParamWhereClause( 2 ) ); QgsDebugMsg( "TopoGeom swap: " + replace ); result = conn->PQprepare( QStringLiteral( "replacetopogeom" ), replace, 2, nullptr ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { QgsDebugMsg( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" ) .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( replace ) ); throw PGException( result ); } } else { update = QStringLiteral( "UPDATE %1 SET %2=%3 WHERE %4" ) .arg( mQuery, quotedIdentifier( mGeometryColumn ), geomParam( 1 ), pkParamWhereClause( 2 ) ); } QgsDebugMsg( "updating: " + update ); result = conn->PQprepare( QStringLiteral( "updatefeatures" ), update, 2, nullptr ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) { QgsDebugMsg( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" ) .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( update ) ); throw PGException( result ); } QgsDebugMsg( QStringLiteral( "iterating over the map of changed geometries..." ) ); for ( QgsGeometryMap::const_iterator iter = geometry_map.constBegin(); iter != geometry_map.constEnd(); ++iter ) { QgsDebugMsg( "iterating over feature id " + FID_TO_STRING( iter.key() ) ); // Save the id of the current topogeometry long old_tg_id = -1; if ( mSpatialColType == SctTopoGeometry ) { QStringList params; appendPkParams( iter.key(), params ); result = connectionRO()->PQexecPrepared( QStringLiteral( "getid" ), params ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { QgsDebugMsg( QStringLiteral( "Exception thrown due to PQexecPrepared of 'getid' returning != PGRES_TUPLES_OK (%1 != expected %2)" ) .arg( result.PQresultStatus() ).arg( PGRES_TUPLES_OK ) ); throw PGException( result ); } // TODO: watch out for NULL, handle somehow old_tg_id = result.PQgetvalue( 0, 0 ).toLong(); QgsDebugMsg( QStringLiteral( "Old TG id is %1" ).arg( old_tg_id ) ); } QStringList params; appendGeomParam( *iter, params ); appendPkParams( iter.key(), params ); result = conn->PQexecPrepared( QStringLiteral( "updatefeatures" ), params ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); if ( mSpatialColType == SctTopoGeometry ) { long new_tg_id = result.PQgetvalue( 0, 0 ).toLong(); // new topogeo_id // Replace old TopoGeom with new TopoGeom, so that // any hierarchically defined TopoGeom will still have its // definition and we'll leave no orphans QString replace = QString( "DELETE FROM %1.relation WHERE " "layer_id = %2 AND topogeo_id = %3" ) .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) ) .arg( mTopoLayerInfo.layerId ) .arg( old_tg_id ); result = conn->PQexec( replace ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { QgsDebugMsg( QStringLiteral( "Exception thrown due to PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" ) .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( replace ) ); throw PGException( result ); } // TODO: use prepared query here replace = QString( "UPDATE %1.relation SET topogeo_id = %2 " "WHERE layer_id = %3 AND topogeo_id = %4" ) .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) ) .arg( old_tg_id ) .arg( mTopoLayerInfo.layerId ) .arg( new_tg_id ); QgsDebugMsg( "relation swap: " + replace ); result = conn->PQexec( replace ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { QgsDebugMsg( QStringLiteral( "Exception thrown due to PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" ) .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( replace ) ); throw PGException( result ); } } // if TopoGeometry } // for each feature conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeatures" ) ); if ( mSpatialColType == SctTopoGeometry ) { connectionRO()->PQexecNR( QStringLiteral( "DEALLOCATE getid" ) ); conn->PQexecNR( QStringLiteral( "DEALLOCATE replacetopogeom" ) ); } returnvalue &= conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); } catch ( PGException &e ) { pushError( tr( "PostGIS error while changing geometry values: %1" ).arg( e.errorMessage() ) ); conn->rollback(); conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeatures" ) ); if ( mSpatialColType == SctTopoGeometry ) { connectionRO()->PQexecNR( QStringLiteral( "DEALLOCATE getid" ) ); conn->PQexecNR( QStringLiteral( "DEALLOCATE replacetopogeom" ) ); } returnvalue = false; } conn->unlock(); QgsDebugMsg( QStringLiteral( "leaving." ) ); return returnvalue; } bool QgsPostgresProvider::changeFeatures( const QgsChangedAttributesMap &attr_map, const QgsGeometryMap &geometry_map ) { Q_ASSERT( mSpatialColType != SctTopoGeometry ); bool returnvalue = true; if ( mIsQuery ) return false; if ( attr_map.isEmpty() ) return true; QgsPostgresConn *conn = connectionRW(); if ( !conn ) return false; conn->lock(); try { conn->begin(); QgsFeatureIds ids( attr_map.keys().toSet() ); ids |= geometry_map.keys().toSet(); // cycle through the features Q_FOREACH ( QgsFeatureId fid, ids ) { // skip added features if ( FID_IS_NEW( fid ) ) continue; const QgsAttributeMap &attrs = attr_map.value( fid ); if ( attrs.isEmpty() && !geometry_map.contains( fid ) ) continue; QString sql = QStringLiteral( "UPDATE %1 SET " ).arg( mQuery ); bool pkChanged = false; // cycle through the changed attributes of the feature QString delim; for ( QgsAttributeMap::const_iterator siter = attrs.constBegin(); siter != attrs.constEnd(); ++siter ) { try { QgsField fld = field( siter.key() ); pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() ); sql += delim + QStringLiteral( "%1=" ).arg( quotedIdentifier( fld.name() ) ); delim = ','; if ( fld.typeName() == QLatin1String( "geometry" ) ) { sql += QStringLiteral( "%1(%2)" ) .arg( connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( siter->toString() ) ); } else if ( fld.typeName() == QLatin1String( "geography" ) ) { sql += QStringLiteral( "st_geographyfromewkt(%1)" ) .arg( quotedValue( siter->toString() ) ); } else { sql += quotedValue( *siter ); } } catch ( PGFieldNotFound ) { // Field was missing - shouldn't happen } } if ( !geometry_map.contains( fid ) ) { sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); } else { sql += QStringLiteral( "%1%2=%3" ).arg( delim, quotedIdentifier( mGeometryColumn ), geomParam( 1 ) ); sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) ); QgsPostgresResult result( conn->PQprepare( QStringLiteral( "updatefeature" ), sql, 1, nullptr ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) { QgsDebugMsg( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" ) .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( sql ) ); throw PGException( result ); } QStringList params; const QgsGeometry &geom = geometry_map[ fid ]; appendGeomParam( geom, params ); result = conn->PQexecPrepared( QStringLiteral( "updatefeature" ), params ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeature" ) ); } // update feature id map if key was changed if ( pkChanged && mPrimaryKeyType == PktFidMap ) { QVariantList k = mShared->removeFid( fid ); for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ ) { int idx = mPrimaryKeyAttrs.at( i ); if ( !attrs.contains( idx ) ) continue; k[i] = attrs[ idx ]; } mShared->insertFid( fid, k ); } } returnvalue &= conn->commit(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); } catch ( PGException &e ) { pushError( tr( "PostGIS error while changing attributes: %1" ).arg( e.errorMessage() ) ); conn->rollback(); returnvalue = false; } conn->unlock(); QgsDebugMsg( QStringLiteral( "leaving." ) ); return returnvalue; } QgsAttributeList QgsPostgresProvider::attributeIndexes() const { QgsAttributeList lst; lst.reserve( mAttributeFields.count() ); for ( int i = 0; i < mAttributeFields.count(); ++i ) lst.append( i ); return lst; } QgsVectorDataProvider::Capabilities QgsPostgresProvider::capabilities() const { return mEnabledCapabilities; } bool QgsPostgresProvider::setSubsetString( const QString &theSQL, bool updateFeatureCount ) { if ( theSQL.trimmed() == mSqlWhereClause ) return true; QString prevWhere = mSqlWhereClause; mSqlWhereClause = theSQL.trimmed(); QString sql = QStringLiteral( "SELECT * FROM %1" ).arg( mQuery ); if ( !mSqlWhereClause.isEmpty() ) { sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause ); } sql += QLatin1String( " LIMIT 0" ); QgsPostgresResult res( connectionRO()->PQexec( sql ) ); if ( res.PQresultStatus() != PGRES_TUPLES_OK ) { pushError( res.PQresultErrorMessage() ); mSqlWhereClause = prevWhere; return false; } #if 0 // FIXME if ( mPrimaryKeyType == PktInt && !uniqueData( 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( false ) ); if ( updateFeatureCount ) { mShared->setFeaturesCounted( -1 ); } mLayerExtent.setMinimal(); emit dataChanged(); return true; } /** * Returns the feature count */ long QgsPostgresProvider::featureCount() const { int featuresCounted = mShared->featuresCounted(); if ( featuresCounted >= 0 ) return featuresCounted; // See: https://issues.qgis.org/issues/17388 - QGIS crashes on featureCount()) if ( ! connectionRO() ) { return 0; } // 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 = QStringLiteral( "SELECT reltuples::int FROM pg_catalog.pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); } else { sql = QStringLiteral( "SELECT count(*) FROM %1%2" ).arg( mQuery, filterWhereClause() ); } QgsPostgresResult result( connectionRO()->PQexec( sql ) ); QgsDebugMsg( "number of features as text: " + result.PQgetvalue( 0, 0 ) ); long num = result.PQgetvalue( 0, 0 ).toLong(); mShared->setFeaturesCounted( num ); QgsDebugMsg( "number of features: " + QString::number( num ) ); return num; } bool QgsPostgresProvider::empty() const { QString sql = QStringLiteral( "SELECT EXISTS (SELECT * FROM %1%2 LIMIT 1)" ).arg( mQuery, filterWhereClause() ); QgsPostgresResult res( connectionRO()->PQexec( sql ) ); if ( res.PQresultStatus() != PGRES_TUPLES_OK ) { pushError( res.PQresultErrorMessage() ); return false; } return res.PQgetvalue( 0, 0 ) != QLatin1String( "t" ); } QgsRectangle QgsPostgresProvider::extent() const { if ( mGeometryColumn.isNull() ) return QgsRectangle(); if ( mSpatialColType == SctGeography ) 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 = QStringLiteral( "SELECT count(*) FROM pg_stats WHERE schemaname=%1 AND tablename=%2 AND attname=%3" ) .arg( quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( mGeometryColumn ) ); result = connectionRO()->PQexec( sql ); if ( result.PQresultStatus() == PGRES_TUPLES_OK && result.PQntuples() == 1 ) { if ( result.PQgetvalue( 0, 0 ).toInt() > 0 ) { sql = QStringLiteral( "SELECT reltuples::int FROM pg_catalog.pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); result = connectionRO()->PQexec( sql ); if ( result.PQresultStatus() == PGRES_TUPLES_OK && result.PQntuples() == 1 && result.PQgetvalue( 0, 0 ).toLong() > 0 ) { sql = QStringLiteral( "SELECT %1(%2,%3,%4)" ) .arg( connectionRO()->majorVersion() < 2 ? "estimated_extent" : ( connectionRO()->majorVersion() == 2 && connectionRO()->minorVersion() < 1 ? "st_estimated_extent" : "st_estimatedextent" ), quotedValue( mSchemaName ), quotedValue( mTableName ), 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( QLatin1String( "-180 " ) ) && ext.contains( QLatin1String( ",180 " ) ) ) { ext.clear(); } } } else { // no features => ignore estimated extent ext.clear(); } } } else { QgsDebugMsg( QStringLiteral( "no column statistics for %1.%2.%3" ).arg( mSchemaName, mTableName, mGeometryColumn ) ); } } if ( ext.isEmpty() ) { sql = QStringLiteral( "SELECT %1(%2%3) FROM %4%5" ) .arg( connectionRO()->majorVersion() < 2 ? "extent" : "st_extent", quotedIdentifier( mGeometryColumn ), mSpatialColType == SctPcPatch ? "::geometry" : "", mQuery, filterWhereClause() ); result = connectionRO()->PQexec( sql ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) connectionRO()->PQexecNR( QStringLiteral( "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; } void QgsPostgresProvider::updateExtents() { mLayerExtent.setMinimal(); } bool QgsPostgresProvider::getGeometryDetails() { if ( mGeometryColumn.isNull() ) { mDetectedGeomType = QgsWkbTypes::NoGeometry; mValid = true; return true; } QgsPostgresResult result; QString sql; QString schemaName = mSchemaName; QString tableName = mTableName; QString geomCol = mGeometryColumn; QString geomColType; if ( mIsQuery ) { sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery ); QgsDebugMsg( QStringLiteral( "Getting geometry column: %1" ).arg( sql ) ); QgsPostgresResult result( connectionRO()->PQexec( sql ) ); if ( PGRES_TUPLES_OK == result.PQresultStatus() ) { Oid tableoid = result.PQftable( 0 ); int column = result.PQftablecol( 0 ); result = connectionRO()->PQexec( sql ); if ( tableoid > 0 && PGRES_TUPLES_OK == result.PQresultStatus() ) { sql = QStringLiteral( "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 == result.PQresultStatus() && 1 == result.PQntuples() ) { schemaName = result.PQgetvalue( 0, 0 ); tableName = result.PQgetvalue( 0, 1 ); sql = QStringLiteral( "SELECT a.attname, t.typname FROM pg_attribute a, pg_type t WHERE a.attrelid=%1 AND a.attnum=%2 AND a.atttypid = t.oid" ).arg( tableoid ).arg( column ); result = connectionRO()->PQexec( sql ); if ( PGRES_TUPLES_OK == result.PQresultStatus() && 1 == result.PQntuples() ) { geomCol = result.PQgetvalue( 0, 0 ); geomColType = result.PQgetvalue( 0, 1 ); if ( geomColType == QLatin1String( "geometry" ) ) mSpatialColType = SctGeometry; else if ( geomColType == QLatin1String( "geography" ) ) mSpatialColType = SctGeography; else if ( geomColType == QLatin1String( "topogeometry" ) ) mSpatialColType = SctTopoGeometry; else if ( geomColType == QLatin1String( "pcpatch" ) ) mSpatialColType = SctPcPatch; else mSpatialColType = SctNone; } else { schemaName = mSchemaName; tableName = mTableName; } } } else { schemaName.clear(); tableName = mQuery; } } else { mValid = false; return false; } } QString detectedType; QString detectedSrid = mRequestedSrid; if ( !schemaName.isEmpty() ) { // check geometry columns sql = QStringLiteral( "SELECT upper(type),srid,coord_dimension FROM geometry_columns WHERE f_table_name=%1 AND f_geometry_column=%2 AND f_table_schema=%3" ) .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) ); QgsDebugMsg( QStringLiteral( "Getting geometry column: %1" ).arg( sql ) ); result = connectionRO()->PQexec( sql ); QgsDebugMsg( QStringLiteral( "Geometry column query returned %1 rows" ).arg( result.PQntuples() ) ); if ( result.PQntuples() == 1 ) { detectedType = result.PQgetvalue( 0, 0 ); QString dim = result.PQgetvalue( 0, 2 ); if ( dim == QLatin1String( "3" ) && !detectedType.endsWith( 'M' ) ) detectedType += QLatin1String( "Z" ); else if ( dim == QLatin1String( "4" ) ) detectedType += QLatin1String( "ZM" ); detectedSrid = result.PQgetvalue( 0, 1 ); mSpatialColType = SctGeometry; } else { connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) ); } if ( detectedType.isEmpty() ) { // check geography columns sql = QStringLiteral( "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 ), quotedValue( geomCol ), quotedValue( schemaName ) ); QgsDebugMsg( QStringLiteral( "Getting geography column: %1" ).arg( sql ) ); result = connectionRO()->PQexec( sql, false ); QgsDebugMsg( QStringLiteral( "Geography column query returned %1" ).arg( result.PQntuples() ) ); if ( result.PQntuples() == 1 ) { detectedType = result.PQgetvalue( 0, 0 ); detectedSrid = result.PQgetvalue( 0, 1 ); mSpatialColType = SctGeography; } else { connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) ); } } if ( detectedType.isEmpty() && connectionRO()->hasTopology() ) { // check topology.layer sql = QString( "SELECT CASE " "WHEN l.feature_type = 1 THEN 'MULTIPOINT' " "WHEN l.feature_type = 2 THEN 'MULTILINESTRING' " "WHEN l.feature_type = 3 THEN 'MULTIPOLYGON' " "WHEN l.feature_type = 4 THEN 'GEOMETRYCOLLECTION' " "END AS type, t.srid FROM topology.layer l, topology.topology t " "WHERE l.topology_id = t.id AND l.schema_name=%3 " "AND l.table_name=%1 AND l.feature_column=%2" ) .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) ); QgsDebugMsg( QStringLiteral( "Getting TopoGeometry column: %1" ).arg( sql ) ); result = connectionRO()->PQexec( sql, false ); QgsDebugMsg( QStringLiteral( "TopoGeometry column query returned %1" ).arg( result.PQntuples() ) ); if ( result.PQntuples() == 1 ) { detectedType = result.PQgetvalue( 0, 0 ); detectedSrid = result.PQgetvalue( 0, 1 ); mSpatialColType = SctTopoGeometry; } else { connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) ); } } if ( detectedType.isEmpty() && connectionRO()->hasPointcloud() ) { // check pointcloud columns sql = QStringLiteral( "SELECT 'POLYGON',srid FROM pointcloud_columns WHERE \"table\"=%1 AND \"column\"=%2 AND \"schema\"=%3" ) .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) ); QgsDebugMsg( QStringLiteral( "Getting pointcloud column: %1" ).arg( sql ) ); result = connectionRO()->PQexec( sql, false ); QgsDebugMsg( QStringLiteral( "Pointcloud column query returned %1" ).arg( result.PQntuples() ) ); if ( result.PQntuples() == 1 ) { detectedType = result.PQgetvalue( 0, 0 ); detectedSrid = result.PQgetvalue( 0, 1 ); mSpatialColType = SctPcPatch; } else { connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) ); } } if ( mSpatialColType == SctNone ) { sql = QString( "SELECT t.typname FROM " "pg_attribute a, pg_class c, pg_namespace n, pg_type t " "WHERE a.attrelid=c.oid AND c.relnamespace=n.oid " "AND a.atttypid=t.oid " "AND n.nspname=%3 AND c.relname=%1 AND a.attname=%2" ) .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) ); QgsDebugMsg( QStringLiteral( "Getting column datatype: %1" ).arg( sql ) ); result = connectionRO()->PQexec( sql, false ); QgsDebugMsg( QStringLiteral( "Column datatype query returned %1" ).arg( result.PQntuples() ) ); if ( result.PQntuples() == 1 ) { geomColType = result.PQgetvalue( 0, 0 ); if ( geomColType == QLatin1String( "geometry" ) ) mSpatialColType = SctGeometry; else if ( geomColType == QLatin1String( "geography" ) ) mSpatialColType = SctGeography; else if ( geomColType == QLatin1String( "topogeometry" ) ) mSpatialColType = SctTopoGeometry; else if ( geomColType == QLatin1String( "pcpatch" ) ) mSpatialColType = SctPcPatch; } else { connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) ); } } } else { sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery ); result = connectionRO()->PQexec( sql ); if ( PGRES_TUPLES_OK == result.PQresultStatus() ) { sql = QStringLiteral( "SELECT (SELECT t.typname FROM pg_type t WHERE oid = %1), upper(postgis_typmod_type(%2)), postgis_typmod_srid(%2)" ) .arg( QString::number( result.PQftype( 0 ) ), QString::number( result.PQfmod( 0 ) ) ); result = connectionRO()->PQexec( sql, false ); if ( result.PQntuples() == 1 ) { geomColType = result.PQgetvalue( 0, 0 ); detectedType = result.PQgetvalue( 0, 1 ); detectedSrid = result.PQgetvalue( 0, 2 ); if ( geomColType == QLatin1String( "geometry" ) ) mSpatialColType = SctGeometry; else if ( geomColType == QLatin1String( "geography" ) ) mSpatialColType = SctGeography; else if ( geomColType == QLatin1String( "topogeometry" ) ) mSpatialColType = SctTopoGeometry; else if ( geomColType == QLatin1String( "pcpatch" ) ) mSpatialColType = SctPcPatch; else { detectedType = mRequestedGeomType == QgsWkbTypes::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType ); detectedSrid = mRequestedSrid; } } else { connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) ); detectedType = mRequestedGeomType == QgsWkbTypes::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType ); } } else { mValid = false; return false; } } mDetectedGeomType = QgsPostgresConn::wkbTypeFromPostgis( detectedType ); mDetectedSrid = detectedSrid; if ( mDetectedGeomType == QgsWkbTypes::Unknown ) { mDetectedSrid.clear(); QgsPostgresLayerProperty layerProperty; if ( !mIsQuery ) { layerProperty.schemaName = schemaName; layerProperty.tableName = tableName; } else { layerProperty.schemaName.clear(); layerProperty.tableName = mQuery; } layerProperty.geometryColName = mGeometryColumn; layerProperty.geometryColType = mSpatialColType; QString delim; if ( !mSqlWhereClause.isEmpty() ) { layerProperty.sql += delim + '(' + mSqlWhereClause + ')'; delim = QStringLiteral( " AND " ); } connectionRO()->retrieveLayerTypes( layerProperty, mUseEstimatedMetadata ); mSpatialColType = layerProperty.geometryColType; if ( layerProperty.size() == 0 ) { // no data - so take what's requested if ( mRequestedGeomType == QgsWkbTypes::Unknown || mRequestedSrid.isEmpty() ) { QgsMessageLog::logMessage( tr( "Geometry type and srid for empty column %1 of %2 undefined." ).arg( mGeometryColumn, mQuery ) ); } } else { int i; for ( i = 0; i < layerProperty.size(); i++ ) { QgsWkbTypes::Type wkbType = layerProperty.types.at( i ); if ( ( wkbType != QgsWkbTypes::Unknown && ( mRequestedGeomType == QgsWkbTypes::Unknown || mRequestedGeomType == wkbType ) ) && ( mRequestedSrid.isEmpty() || layerProperty.srids.at( i ) == mRequestedSrid.toInt() ) ) break; } // requested type && srid is available if ( i < layerProperty.size() ) { if ( layerProperty.size() == 1 ) { // only what we requested is available mDetectedGeomType = layerProperty.types.at( 0 ); mDetectedSrid = QString::number( layerProperty.srids.at( 0 ) ); } } else { // geometry type undetermined or not unrequested QgsMessageLog::logMessage( tr( "Feature type or srid for %1 of %2 could not be determined or was not requested." ).arg( mGeometryColumn, mQuery ) ); } } } QgsDebugMsg( QStringLiteral( "Detected SRID is %1" ).arg( mDetectedSrid ) ); QgsDebugMsg( QStringLiteral( "Requested SRID is %1" ).arg( mRequestedSrid ) ); QgsDebugMsg( QStringLiteral( "Detected type is %1" ).arg( mDetectedGeomType ) ); QgsDebugMsg( QStringLiteral( "Requested type is %1" ).arg( mRequestedGeomType ) ); mValid = ( mDetectedGeomType != QgsWkbTypes::Unknown || mRequestedGeomType != QgsWkbTypes::Unknown ) && ( !mDetectedSrid.isEmpty() || !mRequestedSrid.isEmpty() ); if ( !mValid ) return false; QgsDebugMsg( QStringLiteral( "Spatial column type is %1" ).arg( QgsPostgresConn::displayStringForGeomType( mSpatialColType ) ) ); return mValid; } bool QgsPostgresProvider::convertField( QgsField &field, const QMap *options ) { //determine field type to use for strings QString stringFieldType = QStringLiteral( "varchar" ); if ( options && options->value( QStringLiteral( "dropStringConstraints" ), false ).toBool() ) { //drop string length constraints by using PostgreSQL text type for strings stringFieldType = QStringLiteral( "text" ); } QString fieldType = stringFieldType; //default to string int fieldSize = field.length(); int fieldPrec = field.precision(); switch ( field.type() ) { case QVariant::LongLong: fieldType = QStringLiteral( "int8" ); fieldPrec = 0; break; case QVariant::DateTime: fieldType = QStringLiteral( "timestamp without time zone" ); break; case QVariant::Time: fieldType = QStringLiteral( "time" ); break; case QVariant::String: fieldType = stringFieldType; fieldPrec = -1; break; case QVariant::Int: fieldType = QStringLiteral( "int4" ); fieldPrec = 0; break; case QVariant::Date: fieldType = QStringLiteral( "date" ); fieldPrec = 0; break; case QVariant::Map: fieldType = field.typeName(); if ( fieldType.isEmpty() ) fieldType = QStringLiteral( "hstore" ); fieldPrec = -1; break; case QVariant::StringList: fieldType = QStringLiteral( "_text" ); fieldPrec = -1; break; case QVariant::List: { QgsField sub( QString(), field.subType(), QString(), fieldSize, fieldPrec ); if ( !convertField( sub, nullptr ) ) return false; fieldType = "_" + sub.typeName(); fieldPrec = -1; break; } case QVariant::Double: if ( fieldSize > 18 ) { fieldType = QStringLiteral( "numeric" ); fieldSize = -1; } else { fieldType = QStringLiteral( "float8" ); } fieldPrec = -1; break; case QVariant::Bool: fieldType = QStringLiteral( "bool" ); fieldPrec = -1; fieldSize = -1; break; default: return false; } field.setTypeName( fieldType ); field.setLength( fieldSize ); field.setPrecision( fieldPrec ); return true; } QgsVectorLayerExporter::ExportError QgsPostgresProvider::createEmptyLayer( const QString &uri, const QgsFields &fields, QgsWkbTypes::Type wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap *oldToNewAttrIdxMap, QString *errorMessage, const QMap *options ) { // populate members from the uri structure QgsDataSourceUri dsUri( uri ); QString schemaName = dsUri.schema(); QString tableName = dsUri.table(); QString geometryColumn = dsUri.geometryColumn(); QString geometryType; QString primaryKey = dsUri.keyColumn(); QString primaryKeyType; QStringList pkList; QStringList pkType; QString schemaTableName; if ( !schemaName.isEmpty() ) { schemaTableName += quotedIdentifier( schemaName ) + '.'; } schemaTableName += quotedIdentifier( tableName ); QgsDebugMsg( QStringLiteral( "Connection info is: %1" ).arg( dsUri.connectionInfo( false ) ) ); QgsDebugMsg( QStringLiteral( "Geometry column is: %1" ).arg( geometryColumn ) ); QgsDebugMsg( QStringLiteral( "Schema is: %1" ).arg( schemaName ) ); QgsDebugMsg( QStringLiteral( "Table name is: %1" ).arg( tableName ) ); // create the table QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); if ( !conn ) { if ( errorMessage ) *errorMessage = QObject::tr( "Connection to database failed" ); return QgsVectorLayerExporter::ErrConnectionFailed; } // get the pk's name and type // if no pk name was passed, define the new pk field name if ( primaryKey.isEmpty() ) { int index = 0; QString pk = primaryKey = QStringLiteral( "id" ); for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx ) { if ( fields.at( fldIdx ).name() == primaryKey ) { // it already exists, try again with a new name primaryKey = QStringLiteral( "%1_%2" ).arg( pk ).arg( index++ ); fldIdx = -1; // it is incremented in the for loop, i.e. restarts at 0 } } pkList = QStringList( primaryKey ); pkType = QStringList( QStringLiteral( "serial" ) ); } else { pkList = parseUriKey( primaryKey ); Q_FOREACH ( const QString &col, pkList ) { // search for the passed field QString type; for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx ) { if ( fields[fldIdx].name() == col ) { // found, get the field type QgsField fld = fields[fldIdx]; if ( convertField( fld, options ) ) { type = fld.typeName(); break; } } } if ( type.isEmpty() ) type = QStringLiteral( "serial" ); else { // if the pk field's type is one of the postgres integer types, // use the equivalent autoincremental type (serialN) if ( primaryKeyType == QLatin1String( "int2" ) || primaryKeyType == QLatin1String( "int4" ) ) { primaryKeyType = QStringLiteral( "serial" ); } else if ( primaryKeyType == QLatin1String( "int8" ) ) { primaryKeyType = QStringLiteral( "serial8" ); } } pkType << type; } } try { conn->PQexecNR( QStringLiteral( "BEGIN" ) ); // We want a valid schema name ... if ( schemaName.isEmpty() ) { QString sql = QString( "SELECT current_schema" ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); schemaName = result.PQgetvalue( 0, 0 ); if ( schemaName.isEmpty() ) { schemaName = QStringLiteral( "public" ); } } 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 ), quotedValue( schemaName ) ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( 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=%2 AND nsp.nspname=%1" ) .arg( quotedValue( schemaName ), quotedValue( tableName ) ); result = conn->PQexec( sql ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); } sql = QStringLiteral( "CREATE TABLE %1(" ) .arg( schemaTableName ); QString pk; for ( int i = 0; i < pkList.size(); ++i ) { QString col = pkList[i]; const QString &type = pkType[i]; if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() ) { col = col.toLower(); } else { col = quotedIdentifier( col ); // no need to quote lowercase field } if ( i ) { pk += QLatin1String( "," ); sql += QLatin1String( "," ); } pk += col; sql += col + " " + type; } sql += QStringLiteral( ", PRIMARY KEY (%1) )" ) .arg( pk ); result = conn->PQexec( sql ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) throw PGException( result ); // get geometry type, dim and srid int dim = 2; long srid = srs.postgisSrid(); QgsPostgresConn::postgisWkbType( wkbType, geometryType, dim ); // create geometry column if ( !geometryType.isEmpty() ) { sql = QStringLiteral( "SELECT AddGeometryColumn(%1,%2,%3,%4,%5,%6)" ) .arg( quotedValue( schemaName ), quotedValue( tableName ), quotedValue( geometryColumn ) ) .arg( srid ) .arg( quotedValue( geometryType ) ) .arg( dim ); result = conn->PQexec( sql ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) throw PGException( result ); } else { geometryColumn.clear(); } conn->PQexecNR( QStringLiteral( "COMMIT" ) ); } catch ( PGException &e ) { if ( errorMessage ) *errorMessage = QObject::tr( "Creation of data source %1 failed: \n%2" ) .arg( schemaTableName, e.errorMessage() ); conn->PQexecNR( QStringLiteral( "ROLLBACK" ) ); conn->unref(); return QgsVectorLayerExporter::ErrCreateLayer; } conn->unref(); QgsDebugMsg( QStringLiteral( "layer %1 created" ).arg( schemaTableName ) ); // use the provider to edit the table dsUri.setDataSource( schemaName, tableName, geometryColumn, QString(), primaryKey ); QgsDataProvider::ProviderOptions providerOptions; std::unique_ptr< QgsPostgresProvider > provider = qgis::make_unique< QgsPostgresProvider >( dsUri.uri( false ), providerOptions ); if ( !provider->isValid() ) { if ( errorMessage ) *errorMessage = QObject::tr( "Loading of the layer %1 failed" ).arg( schemaTableName ); return QgsVectorLayerExporter::ErrInvalidLayer; } QgsDebugMsg( QStringLiteral( "layer loaded" ) ); // add fields to the layer if ( oldToNewAttrIdxMap ) oldToNewAttrIdxMap->clear(); if ( fields.size() > 0 ) { int offset = 1; // get the list of fields QList flist; for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx ) { QgsField fld = fields.at( fldIdx ); if ( fld.name() == geometryColumn ) { //the "lowercaseFieldNames" option does not affect the name of the geometry column, so we perform //this test before converting the field name to lowercase QgsDebugMsg( QStringLiteral( "Found a field with the same name of the geometry column. Skip it!" ) ); continue; } if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() ) { //convert field name to lowercase fld.setName( fld.name().toLower() ); } int pkIdx = -1; for ( int i = 0; i < pkList.size(); ++i ) { QString col = pkList[i]; if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() ) { //convert field name to lowercase (TODO: avoid doing this //over and over) col = col.toLower(); } if ( fld.name() == col ) { pkIdx = i; break; } } if ( pkIdx >= 0 ) { oldToNewAttrIdxMap->insert( fldIdx, pkIdx ); continue; } if ( !convertField( fld, options ) ) { if ( errorMessage ) *errorMessage = QObject::tr( "Unsupported type for field %1" ).arg( fld.name() ); return QgsVectorLayerExporter::ErrAttributeTypeUnsupported; } QgsDebugMsg( QStringLiteral( "creating field #%1 -> #%2 name %3 type %4 typename %5 width %6 precision %7" ) .arg( fldIdx ).arg( offset ) .arg( fld.name(), QVariant::typeToName( fld.type() ), fld.typeName() ) .arg( fld.length() ).arg( fld.precision() ) ); flist.append( fld ); if ( oldToNewAttrIdxMap ) oldToNewAttrIdxMap->insert( fldIdx, offset++ ); } if ( !provider->addAttributes( flist ) ) { if ( errorMessage ) *errorMessage = QObject::tr( "Creation of fields failed" ); return QgsVectorLayerExporter::ErrAttributeCreationFailed; } QgsDebugMsg( QStringLiteral( "Done creating fields" ) ); } return QgsVectorLayerExporter::NoError; } QgsCoordinateReferenceSystem QgsPostgresProvider::crs() const { QgsCoordinateReferenceSystem srs; int srid = mRequestedSrid.isEmpty() ? mDetectedSrid.toInt() : mRequestedSrid.toInt(); srs.createFromSrid( srid ); if ( !srs.isValid() ) { static QMutex sMutex; QMutexLocker locker( &sMutex ); static QMap sCrsCache; if ( sCrsCache.contains( srid ) ) srs = sCrsCache.value( srid ); else { QgsPostgresConn *conn = connectionRO(); conn->lock(); QgsPostgresResult result( conn->PQexec( QStringLiteral( "SELECT proj4text FROM spatial_ref_sys WHERE srid=%1" ).arg( srid ) ) ); conn->unlock(); if ( result.PQresultStatus() == PGRES_TUPLES_OK ) { srs = QgsCoordinateReferenceSystem::fromProj4( result.PQgetvalue( 0, 0 ) ); sCrsCache.insert( srid, srs ); } } } return srs; } QString QgsPostgresProvider::subsetString() const { return mSqlWhereClause; } 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 { QString pgVersion( tr( "PostgreSQL version: unknown" ) ); QString postgisVersion( tr( "unknown" ) ); if ( connectionRO() ) { QgsPostgresResult result; result = connectionRO()->PQexec( QStringLiteral( "SELECT version()" ) ); if ( result.PQresultStatus() == PGRES_TUPLES_OK ) { pgVersion = result.PQgetvalue( 0, 0 ); } result = connectionRO()->PQexec( QStringLiteral( "SELECT postgis_version()" ) ); if ( result.PQresultStatus() == PGRES_TUPLES_OK ) { postgisVersion = result.PQgetvalue( 0, 0 ); } } else { pgVersion = tr( "PostgreSQL not connected" ); } return tr( "PostgreSQL/PostGIS provider\n%1\nPostGIS %2" ).arg( pgVersion, postgisVersion ); } // QgsPostgresProvider::description() static void jumpSpace( const QString &txt, int &i ) { while ( i < txt.length() && txt.at( i ).isSpace() ) ++i; } static QString getNextString( const QString &txt, int &i, const QString &sep ) { jumpSpace( txt, i ); QString cur = txt.mid( i ); if ( cur.startsWith( '"' ) ) { QRegExp stringRe( "^\"((?:\\\\.|[^\"\\\\])*)\".*" ); if ( !stringRe.exactMatch( cur ) ) { QgsLogger::warning( "Cannot find end of double quoted string: " + txt ); return QString(); } i += stringRe.cap( 1 ).length() + 2; jumpSpace( txt, i ); if ( !txt.midRef( i ).startsWith( sep ) && i < txt.length() ) { QgsLogger::warning( "Cannot find separator: " + txt.mid( i ) ); return QString(); } i += sep.length(); return stringRe.cap( 1 ).replace( QLatin1String( "\\\"" ), QLatin1String( "\"" ) ).replace( QLatin1String( "\\\\" ), QLatin1String( "\\" ) ); } else { int sepPos = cur.indexOf( sep ); if ( sepPos < 0 ) { i += cur.length(); return cur.trimmed(); } i += sepPos + sep.length(); return cur.left( sepPos ).trimmed(); } } static QVariant parseHstore( const QString &txt ) { QVariantMap result; int i = 0; while ( i < txt.length() ) { QString key = getNextString( txt, i, QStringLiteral( "=>" ) ); QString value = getNextString( txt, i, QStringLiteral( "," ) ); if ( key.isNull() || value.isNull() ) { QgsLogger::warning( "Error parsing hstore: " + txt ); break; } result.insert( key, value ); } return result; } static QVariant parseJson( const QString &txt ) { QVariant result; QJsonDocument jsonResponse = QJsonDocument::fromJson( txt.toUtf8() ); //it's null if no json format result = jsonResponse.toVariant(); return result; } static QVariant parseOtherArray( const QString &txt, QVariant::Type subType, const QString &typeName ) { int i = 0; QVariantList result; while ( i < txt.length() ) { const QString value = getNextString( txt, i, QStringLiteral( "," ) ); if ( value.isNull() ) { QgsLogger::warning( "Error parsing array: " + txt ); break; } result.append( QgsPostgresProvider::convertValue( subType, QVariant::Invalid, value, typeName ) ); } return result; } static QVariant parseStringArray( const QString &txt ) { int i = 0; QStringList result; while ( i < txt.length() ) { const QString value = getNextString( txt, i, QStringLiteral( "," ) ); if ( value.isNull() ) { QgsLogger::warning( "Error parsing array: " + txt ); break; } result.append( value ); } return result; } static QVariant parseArray( const QString &txt, QVariant::Type type, QVariant::Type subType, const QString &typeName ) { if ( !txt.startsWith( '{' ) || !txt.endsWith( '}' ) ) { if ( !txt.isEmpty() ) QgsLogger::warning( "Error parsing array, missing curly braces: " + txt ); return QVariant( type ); } QString inner = txt.mid( 1, txt.length() - 2 ); if ( type == QVariant::StringList ) return parseStringArray( inner ); else return parseOtherArray( inner, subType, typeName ); } QVariant QgsPostgresProvider::convertValue( QVariant::Type type, QVariant::Type subType, const QString &value, const QString &typeName ) { QVariant result; switch ( type ) { case QVariant::Map: if ( typeName == QLatin1String( "json" ) || typeName == QLatin1String( "jsonb" ) ) result = parseJson( value ); else result = parseHstore( value ); break; case QVariant::StringList: case QVariant::List: result = parseArray( value, type, subType, typeName ); break; case QVariant::Bool: if ( value == QChar( 't' ) ) result = true; else if ( value == QChar( 'f' ) ) result = false; else result = QVariant( type ); break; default: result = value; if ( !result.convert( type ) || value.isNull() ) result = QVariant( type ); break; } return result; } QList QgsPostgresProvider::searchLayers( const QList &layers, const QString &connectionInfo, const QString &schema, const QString &tableName ) { QList result; Q_FOREACH ( QgsVectorLayer *layer, layers ) { const QgsPostgresProvider *pgProvider = qobject_cast( layer->dataProvider() ); if ( pgProvider && pgProvider->mUri.connectionInfo( false ) == connectionInfo && pgProvider->mSchemaName == schema && pgProvider->mTableName == tableName ) { result.append( layer ); } } return result; } QList QgsPostgresProvider::discoverRelations( const QgsVectorLayer *self, const QList &layers ) const { QList result; QString sql( "SELECT RC.CONSTRAINT_NAME, KCU1.COLUMN_NAME, KCU2.CONSTRAINT_SCHEMA, KCU2.TABLE_NAME, KCU2.COLUMN_NAME, KCU1.ORDINAL_POSITION " "FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC " "INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU1 " "ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME " "INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU2 " "ON KCU2.CONSTRAINT_CATALOG = RC.UNIQUE_CONSTRAINT_CATALOG AND KCU2.CONSTRAINT_SCHEMA = RC.UNIQUE_CONSTRAINT_SCHEMA AND KCU2.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME " "AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION " "WHERE KCU1.CONSTRAINT_SCHEMA=" + QgsPostgresConn::quotedValue( mSchemaName ) + " AND KCU1.TABLE_NAME=" + QgsPostgresConn::quotedValue( mTableName ) + "GROUP BY RC.CONSTRAINT_NAME, KCU1.COLUMN_NAME, KCU2.CONSTRAINT_SCHEMA, KCU2.TABLE_NAME, KCU2.COLUMN_NAME, KCU1.ORDINAL_POSITION " + "ORDER BY KCU1.ORDINAL_POSITION" ); QgsPostgresResult sqlResult( connectionRO()->PQexec( sql ) ); if ( sqlResult.PQresultStatus() != PGRES_TUPLES_OK ) { QgsLogger::warning( "Error getting the foreign keys of " + mTableName ); return result; } int nbFound = 0; for ( int row = 0; row < sqlResult.PQntuples(); ++row ) { const QString name = sqlResult.PQgetvalue( row, 0 ); const QString fkColumn = sqlResult.PQgetvalue( row, 1 ); const QString refSchema = sqlResult.PQgetvalue( row, 2 ); const QString refTable = sqlResult.PQgetvalue( row, 3 ); const QString refColumn = sqlResult.PQgetvalue( row, 4 ); const QString position = sqlResult.PQgetvalue( row, 5 ); if ( position == QLatin1String( "1" ) ) { // first reference field => try to find if we have layers for the referenced table const QList foundLayers = searchLayers( layers, mUri.connectionInfo( false ), refSchema, refTable ); Q_FOREACH ( const QgsVectorLayer *foundLayer, foundLayers ) { QgsRelation relation; relation.setName( name ); relation.setReferencingLayer( self->id() ); relation.setReferencedLayer( foundLayer->id() ); relation.addFieldPair( fkColumn, refColumn ); relation.generateId(); if ( relation.isValid() ) { result.append( relation ); ++nbFound; } else { QgsLogger::warning( "Invalid relation for " + name ); } } } else { // multi reference field => add the field pair to all the referenced layers found for ( int i = 0; i < nbFound; ++i ) { result[result.size() - 1 - i].addFieldPair( fkColumn, refColumn ); } } } return result; } QgsAttrPalIndexNameHash QgsPostgresProvider::palAttributeIndexNames() const { return mAttrPalIndexName; } QgsPostgresProvider::Relkind QgsPostgresProvider::relkind() const { if ( mIsQuery ) return Relkind::Unknown; QString sql = QStringLiteral( "SELECT relkind FROM pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); QgsPostgresResult res( connectionRO()->PQexec( sql ) ); QString type = res.PQgetvalue( 0, 0 ); QgsPostgresProvider::Relkind kind = Relkind::Unknown; if ( type == QLatin1String( "r" ) ) { kind = Relkind::OrdinaryTable; } else if ( type == QLatin1String( "i" ) ) { kind = Relkind::Index; } else if ( type == QLatin1String( "s" ) ) { kind = Relkind::Sequence; } else if ( type == QLatin1String( "v" ) ) { kind = Relkind::View; } else if ( type == QLatin1String( "m" ) ) { kind = Relkind::MaterializedView; } else if ( type == QLatin1String( "c" ) ) { kind = Relkind::CompositeType; } else if ( type == QLatin1String( "t" ) ) { kind = Relkind::ToastTable; } else if ( type == QLatin1String( "f" ) ) { kind = Relkind::ForeignTable; } else if ( type == QLatin1String( "p" ) ) { kind = Relkind::PartitionedTable; } return kind; } bool QgsPostgresProvider::hasMetadata() const { bool hasMetadata = true; QgsPostgresProvider::Relkind kind = relkind(); if ( kind == Relkind::View || kind == Relkind::MaterializedView ) { hasMetadata = false; } return hasMetadata; } /** * Class factory to return a pointer to a newly created * QgsPostgresProvider object */ QGISEXTERN QgsPostgresProvider *classFactory( const QString *uri, const QgsDataProvider::ProviderOptions &options ) { return new QgsPostgresProvider( *uri, options ); } /** * Required key function (used to map the plugin to a data store type) */ QGISEXTERN QString providerKey() { return POSTGRES_KEY; } /** * Required description function */ QGISEXTERN QString description() { return POSTGRES_DESCRIPTION; } /** * Required isProvider function. Used to determine if this shared library * is a data provider plugin */ QGISEXTERN bool isProvider() { return true; } #ifdef HAVE_GUI QGISEXTERN QgsPgSourceSelect *selectWidget( QWidget *parent, Qt::WindowFlags fl, QgsProviderRegistry::WidgetMode widgetMode ) { return new QgsPgSourceSelect( parent, fl, widgetMode ); } #endif QGISEXTERN int dataCapabilities() { return QgsDataProvider::Database; } QGISEXTERN QgsDataItem *dataItem( QString path, QgsDataItem *parentItem ) { Q_UNUSED( path ); return new QgsPGRootItem( parentItem, QStringLiteral( "PostGIS" ), QStringLiteral( "pg:" ) ); } // --------------------------------------------------------------------------- QGISEXTERN QgsVectorLayerExporter::ExportError createEmptyLayer( const QString &uri, const QgsFields &fields, QgsWkbTypes::Type wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap *oldToNewAttrIdxMap, QString *errorMessage, const QMap *options ) { return QgsPostgresProvider::createEmptyLayer( uri, fields, wkbType, srs, overwrite, oldToNewAttrIdxMap, errorMessage, options ); } QGISEXTERN bool deleteLayer( const QString &uri, QString &errCause ) { QgsDebugMsg( "deleting layer " + uri ); QgsDataSourceUri dsUri( uri ); QString schemaName = dsUri.schema(); QString tableName = dsUri.table(); QString geometryCol = dsUri.geometryColumn(); QString schemaTableName; if ( !schemaName.isEmpty() ) { schemaTableName = QgsPostgresConn::quotedIdentifier( schemaName ) + '.'; } schemaTableName += QgsPostgresConn::quotedIdentifier( tableName ); QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); if ( !conn ) { errCause = QObject::tr( "Connection to database failed" ); return false; } // check the geometry column count QString sql = QString( "SELECT count(*) " "FROM geometry_columns, pg_class, pg_namespace " "WHERE f_table_name=relname AND f_table_schema=nspname " "AND pg_class.relnamespace=pg_namespace.oid " "AND f_table_schema=%1 AND f_table_name=%2" ) .arg( QgsPostgresConn::quotedValue( schemaName ), QgsPostgresConn::quotedValue( tableName ) ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { errCause = QObject::tr( "Unable to delete layer %1: \n%2" ) .arg( schemaTableName, result.PQresultErrorMessage() ); conn->unref(); return false; } int count = result.PQgetvalue( 0, 0 ).toInt(); if ( !geometryCol.isEmpty() && count > 1 ) { // the table has more geometry columns, drop just the geometry column sql = QStringLiteral( "SELECT DropGeometryColumn(%1,%2,%3)" ) .arg( QgsPostgresConn::quotedValue( schemaName ), QgsPostgresConn::quotedValue( tableName ), QgsPostgresConn::quotedValue( geometryCol ) ); } else { // drop the table sql = QStringLiteral( "SELECT DropGeometryTable(%1,%2)" ) .arg( QgsPostgresConn::quotedValue( schemaName ), QgsPostgresConn::quotedValue( tableName ) ); } result = conn->PQexec( sql ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { errCause = QObject::tr( "Unable to delete layer %1: \n%2" ) .arg( schemaTableName, result.PQresultErrorMessage() ); conn->unref(); return false; } conn->unref(); return true; } QGISEXTERN bool deleteSchema( const QString &schema, const QgsDataSourceUri &uri, QString &errCause, bool cascade = false ) { QgsDebugMsg( "deleting schema " + schema ); if ( schema.isEmpty() ) return false; QString schemaName = QgsPostgresConn::quotedIdentifier( schema ); QgsPostgresConn *conn = QgsPostgresConn::connectDb( uri.connectionInfo( false ), false ); if ( !conn ) { errCause = QObject::tr( "Connection to database failed" ); return false; } // drop the schema QString sql = QStringLiteral( "DROP SCHEMA %1 %2" ) .arg( schemaName, cascade ? QStringLiteral( "CASCADE" ) : QString() ); QgsPostgresResult result( conn->PQexec( sql ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { errCause = QObject::tr( "Unable to delete schema %1: \n%2" ) .arg( schemaName, result.PQresultErrorMessage() ); conn->unref(); return false; } conn->unref(); return true; } QGISEXTERN bool saveStyle( const QString &uri, const QString &qmlStyle, const QString &sldStyle, const QString &styleName, const QString &styleDescription, const QString &uiFileContent, bool useAsDefault, QString &errCause ) { QgsDataSourceUri dsUri( uri ); QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); if ( !conn ) { errCause = QObject::tr( "Connection to database failed" ); return false; } if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) ) { QgsPostgresResult res( conn->PQexec( "CREATE TABLE layer_styles(" "id SERIAL PRIMARY KEY" ",f_table_catalog varchar" ",f_table_schema varchar" ",f_table_name varchar" ",f_geometry_column varchar" ",styleName text" ",styleQML xml" ",styleSLD xml" ",useAsDefault boolean" ",description text" ",owner varchar(63)" ",ui xml" ",update_time timestamp DEFAULT CURRENT_TIMESTAMP" ")" ) ); if ( res.PQresultStatus() != PGRES_COMMAND_OK ) { errCause = QObject::tr( "Unable to save layer style. It's not possible to create the destination table on the database. Maybe this is due to table permissions (user=%1). Please contact your database admin" ).arg( dsUri.username() ); conn->unref(); return false; } } if ( dsUri.database().isEmpty() ) // typically when a service file is used { dsUri.setDatabase( conn->currentDatabase() ); } QString uiFileColumn; QString uiFileValue; if ( !uiFileContent.isEmpty() ) { uiFileColumn = QStringLiteral( ",ui" ); uiFileValue = QStringLiteral( ",XMLPARSE(DOCUMENT %1)" ).arg( QgsPostgresConn::quotedValue( uiFileContent ) ); } // Note: in the construction of the INSERT and UPDATE strings the qmlStyle and sldStyle values // can contain user entered strings, which may themselves include %## values that would be // replaced by the QString.arg function. To ensure that the final SQL string is not corrupt these // two values are both replaced in the final .arg call of the string construction. QString sql = QString( "INSERT INTO layer_styles(" "f_table_catalog,f_table_schema,f_table_name,f_geometry_column,styleName,styleQML,styleSLD,useAsDefault,description,owner%11" ") VALUES (" "%1,%2,%3,%4,%5,XMLPARSE(DOCUMENT %16),XMLPARSE(DOCUMENT %17),%8,%9,%10%12" ")" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ) .arg( useAsDefault ? "true" : "false" ) .arg( QgsPostgresConn::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) ) .arg( QgsPostgresConn::quotedValue( dsUri.username() ) ) .arg( uiFileColumn ) .arg( uiFileValue ) // Must be the final .arg replacement - see above .arg( QgsPostgresConn::quotedValue( qmlStyle ), QgsPostgresConn::quotedValue( sldStyle ) ); QString checkQuery = QString( "SELECT styleName" " FROM layer_styles" " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" " AND f_geometry_column=%4" " AND styleName=%5" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ); QgsPostgresResult res( conn->PQexec( checkQuery ) ); if ( res.PQntuples() > 0 ) { if ( QMessageBox::question( nullptr, QObject::tr( "Save style in database" ), QObject::tr( "A style named \"%1\" already exists in the database for this layer. Do you want to overwrite it?" ) .arg( styleName.isEmpty() ? dsUri.table() : styleName ), QMessageBox::Yes | QMessageBox::No ) == QMessageBox::No ) { errCause = QObject::tr( "Operation aborted. No changes were made in the database" ); conn->unref(); return false; } sql = QString( "UPDATE layer_styles" " SET useAsDefault=%1" ",styleQML=XMLPARSE(DOCUMENT %12)" ",styleSLD=XMLPARSE(DOCUMENT %13)" ",description=%4" ",owner=%5" " WHERE f_table_catalog=%6" " AND f_table_schema=%7" " AND f_table_name=%8" " AND f_geometry_column=%9" " AND styleName=%10" ) .arg( useAsDefault ? "true" : "false" ) .arg( QgsPostgresConn::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) ) .arg( QgsPostgresConn::quotedValue( dsUri.username() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ) // Must be the final .arg replacement - see above .arg( QgsPostgresConn::quotedValue( qmlStyle ), QgsPostgresConn::quotedValue( sldStyle ) ); } if ( useAsDefault ) { QString removeDefaultSql = QString( "UPDATE layer_styles" " SET useAsDefault=false" " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" " AND f_geometry_column=%4" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ); sql = QStringLiteral( "BEGIN; %1; %2; COMMIT;" ).arg( removeDefaultSql, sql ); } res = conn->PQexec( sql ); bool saved = res.PQresultStatus() == PGRES_COMMAND_OK; if ( !saved ) errCause = QObject::tr( "Unable to save layer style. It's not possible to insert a new record into the style table. Maybe this is due to table permissions (user=%1). Please contact your database administrator." ).arg( dsUri.username() ); conn->unref(); return saved; } QGISEXTERN QString loadStyle( const QString &uri, QString &errCause ) { QgsDataSourceUri dsUri( uri ); QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); if ( !conn ) { errCause = QObject::tr( "Connection to database failed" ); return QString(); } if ( dsUri.database().isEmpty() ) // typically when a service file is used { dsUri.setDatabase( conn->currentDatabase() ); } if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) ) { conn->unref(); return QString(); } QString geomColumnExpr; if ( dsUri.geometryColumn().isEmpty() ) { geomColumnExpr = QStringLiteral( "IS NULL" ); } else { geomColumnExpr = QStringLiteral( "=" ) + QgsPostgresConn::quotedValue( dsUri.geometryColumn() ); } QString selectQmlQuery = QString( "SELECT styleQML" " FROM layer_styles" " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" " AND f_geometry_column %4" " ORDER BY CASE WHEN useAsDefault THEN 1 ELSE 2 END" ",update_time DESC LIMIT 1" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) .arg( geomColumnExpr ); QgsPostgresResult result( conn->PQexec( selectQmlQuery ) ); QString style = result.PQntuples() == 1 ? result.PQgetvalue( 0, 0 ) : QString(); conn->unref(); return style; } QGISEXTERN int listStyles( const QString &uri, QStringList &ids, QStringList &names, QStringList &descriptions, QString &errCause ) { QgsDataSourceUri dsUri( uri ); QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); if ( !conn ) { errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() ); return -1; } if ( dsUri.database().isEmpty() ) // typically when a service file is used { dsUri.setDatabase( conn->currentDatabase() ); } QString selectRelatedQuery = QString( "SELECT id,styleName,description" " FROM layer_styles" " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" " AND f_geometry_column=%4" " ORDER BY useasdefault DESC, update_time DESC" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ); QgsPostgresResult result( conn->PQexec( selectRelatedQuery ) ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectRelatedQuery ) ); errCause = QObject::tr( "Error executing the select query for related styles. The query was logged" ); conn->unref(); return -1; } int numberOfRelatedStyles = result.PQntuples(); for ( int i = 0; i < numberOfRelatedStyles; i++ ) { ids.append( result.PQgetvalue( i, 0 ) ); names.append( result.PQgetvalue( i, 1 ) ); descriptions.append( result.PQgetvalue( i, 2 ) ); } QString selectOthersQuery = QString( "SELECT id,styleName,description" " FROM layer_styles" " WHERE NOT (f_table_catalog=%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4)" " ORDER BY update_time DESC" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ); result = conn->PQexec( selectOthersQuery ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) { QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectOthersQuery ) ); errCause = QObject::tr( "Error executing the select query for unrelated styles. The query was logged" ); conn->unref(); return -1; } for ( int i = 0; i < result.PQntuples(); i++ ) { ids.append( result.PQgetvalue( i, 0 ) ); names.append( result.PQgetvalue( i, 1 ) ); descriptions.append( result.PQgetvalue( i, 2 ) ); } conn->unref(); return numberOfRelatedStyles; } QGISEXTERN bool deleteStyleById( const QString &uri, QString styleId, QString &errCause ) { QgsDataSourceUri dsUri( uri ); bool deleted; QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); if ( !conn ) { errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() ); deleted = false; } else { QString deleteStyleQuery = QStringLiteral( "DELETE FROM layer_styles WHERE id=%1" ).arg( QgsPostgresConn::quotedValue( styleId ) ); QgsPostgresResult result( conn->PQexec( deleteStyleQuery ) ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { QgsDebugMsg( QString( "PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" ) .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( deleteStyleQuery ) ); QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( deleteStyleQuery ) ); errCause = QObject::tr( "Error executing the delete query. The query was logged" ); deleted = false; } else { deleted = true; } conn->unref(); } return deleted; } QGISEXTERN QString getStyleById( const QString &uri, QString styleId, QString &errCause ) { QgsDataSourceUri dsUri( uri ); QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); if ( !conn ) { errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() ); return QString(); } QString style; QString selectQmlQuery = QStringLiteral( "SELECT styleQml FROM layer_styles WHERE id=%1" ).arg( QgsPostgresConn::quotedValue( styleId ) ); QgsPostgresResult result( conn->PQexec( selectQmlQuery ) ); if ( result.PQresultStatus() == PGRES_TUPLES_OK ) { if ( result.PQntuples() == 1 ) style = result.PQgetvalue( 0, 0 ); else errCause = QObject::tr( "Consistency error in table '%1'. Style id should be unique" ).arg( QStringLiteral( "layer_styles" ) ); } else { QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectQmlQuery ) ); errCause = QObject::tr( "Error executing the select query. The query was logged" ); } conn->unref(); return style; } QGISEXTERN QgsTransaction *createTransaction( const QString &connString ) { return new QgsPostgresTransaction( connString ); } QgsPostgresProjectStorage *gProjectStorage = nullptr; // when not null it is owned by QgsApplication::projectStorageRegistry() QGISEXTERN void initProvider() { Q_ASSERT( !gProjectStorage ); gProjectStorage = new QgsPostgresProjectStorage; QgsApplication::projectStorageRegistry()->registerProjectStorage( gProjectStorage ); // takes ownership } QGISEXTERN void cleanupProvider() { QgsApplication::projectStorageRegistry()->unregisterProjectStorage( gProjectStorage ); // destroys the object gProjectStorage = nullptr; QgsPostgresConnPool::cleanupInstance(); } #ifdef HAVE_GUI //! Provider for postgres source select class QgsPostgresSourceSelectProvider : public QgsSourceSelectProvider //#spellok { public: QString providerKey() const override { return QStringLiteral( "postgres" ); } QString text() const override { return QObject::tr( "PostgreSQL" ); } int ordering() const override { return QgsSourceSelectProvider::OrderDatabaseProvider + 20; } QIcon icon() const override { return QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddPostgisLayer.svg" ) ); } QgsAbstractDataSourceWidget *createDataSourceWidget( QWidget *parent = nullptr, Qt::WindowFlags fl = Qt::Widget, QgsProviderRegistry::WidgetMode widgetMode = QgsProviderRegistry::WidgetMode::Embedded ) const override { return new QgsPgSourceSelect( parent, fl, widgetMode ); } }; QGISEXTERN QList *sourceSelectProviders() { QList *providers = new QList(); *providers << new QgsPostgresSourceSelectProvider; //#spellok return providers; } #endif // ---------- void QgsPostgresSharedData::addFeaturesCounted( long diff ) { QMutexLocker locker( &mMutex ); if ( mFeaturesCounted >= 0 ) mFeaturesCounted += diff; } void QgsPostgresSharedData::ensureFeaturesCountedAtLeast( long fetched ) { QMutexLocker locker( &mMutex ); /* only updates the feature count if it was already once. * Otherwise, this would lead to false feature count if * an existing project is open at a restrictive extent. */ if ( mFeaturesCounted > 0 && mFeaturesCounted < fetched ) { QgsDebugMsg( QStringLiteral( "feature count adjusted from %1 to %2" ).arg( mFeaturesCounted ).arg( fetched ) ); mFeaturesCounted = fetched; } } long QgsPostgresSharedData::featuresCounted() { QMutexLocker locker( &mMutex ); return mFeaturesCounted; } void QgsPostgresSharedData::setFeaturesCounted( long count ) { QMutexLocker locker( &mMutex ); mFeaturesCounted = count; } QgsFeatureId QgsPostgresSharedData::lookupFid( const QVariantList &v ) { QMutexLocker locker( &mMutex ); QMap::const_iterator it = mKeyToFid.constFind( v ); if ( it != mKeyToFid.constEnd() ) { return it.value(); } mFidToKey.insert( ++mFidCounter, v ); mKeyToFid.insert( v, mFidCounter ); return mFidCounter; } QVariantList QgsPostgresSharedData::removeFid( QgsFeatureId fid ) { QMutexLocker locker( &mMutex ); QVariantList v = mFidToKey[ fid ]; mFidToKey.remove( fid ); mKeyToFid.remove( v ); return v; } void QgsPostgresSharedData::insertFid( QgsFeatureId fid, const QVariantList &k ) { QMutexLocker locker( &mMutex ); mFidToKey.insert( fid, k ); mKeyToFid.insert( k, fid ); } QVariantList QgsPostgresSharedData::lookupKey( QgsFeatureId featureId ) { QMutexLocker locker( &mMutex ); QMap::const_iterator it = mFidToKey.constFind( featureId ); if ( it != mFidToKey.constEnd() ) return it.value(); return QVariantList(); } void QgsPostgresSharedData::clear() { QMutexLocker locker( &mMutex ); mFidToKey.clear(); mKeyToFid.clear(); mFeaturesCounted = -1; mFidCounter = 0; }