mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-03 00:04:47 -04:00
5905 lines
208 KiB
C++
5905 lines
208 KiB
C++
/***************************************************************************
|
|
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 "qgsmessagelog.h"
|
|
#include "qgsprojectstorageregistry.h"
|
|
#include "qgslayermetadataproviderregistry.h"
|
|
#include "qgsrectangle.h"
|
|
#include "qgscoordinatereferencesystem.h"
|
|
#include "qgsxmlutils.h"
|
|
#include "qgsvectorlayer.h"
|
|
#include "qgspostgresprovider.h"
|
|
#include "moc_qgspostgresprovider.cpp"
|
|
#include "qgspostgresconn.h"
|
|
#include "qgspostgresconnpool.h"
|
|
#include "qgspostgresdataitems.h"
|
|
#include "qgspostgresfeatureiterator.h"
|
|
#include "qgspostgrestransaction.h"
|
|
#include "qgspostgreslistener.h"
|
|
#include "qgspostgresprojectstorage.h"
|
|
#include "qgspostgresproviderconnection.h"
|
|
#include "qgslogger.h"
|
|
#include "qgsfeedback.h"
|
|
#include "qgsstringutils.h"
|
|
#include "qgsjsonutils.h"
|
|
#include "qgsdbquerylog.h"
|
|
#include "qgsdbquerylog_p.h"
|
|
#include "qgspostgreslayermetadataprovider.h"
|
|
#include "qgsthreadingutils.h"
|
|
|
|
#include "qgspostgresprovider.h"
|
|
#include "qgsprovidermetadata.h"
|
|
#include "qgspostgresproviderconnection.h"
|
|
#include "qgspostgresprovidermetadatautils.h"
|
|
#include "qgspostgresutils.h"
|
|
#include <QRegularExpression>
|
|
|
|
const QString QgsPostgresProvider::POSTGRES_KEY = QStringLiteral( "postgres" );
|
|
const QString QgsPostgresProvider::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 );
|
|
}
|
|
|
|
QgsPostgresPrimaryKeyType
|
|
QgsPostgresProvider::pkType( const QgsField &f ) const
|
|
{
|
|
switch ( f.type() )
|
|
{
|
|
case QMetaType::Type::LongLong:
|
|
// PostgreSQL doesn't have native "unsigned" types.
|
|
// Unsigned primary keys are emulated by the serial/bigserial
|
|
// pseudo-types, in which autogenerated values are always > 0;
|
|
// however, the database accepts manually inserted 0 and negative values
|
|
// in these fields.
|
|
return PktInt64;
|
|
|
|
case QMetaType::Type::Int:
|
|
return PktInt;
|
|
|
|
default:
|
|
return PktFidMap;
|
|
}
|
|
}
|
|
|
|
|
|
QgsPostgresProvider::QgsPostgresProvider( QString const &uri, const ProviderOptions &options, Qgis::DataProviderReadFlags flags )
|
|
: QgsVectorDataProvider( uri, options, flags )
|
|
, mShared( new QgsPostgresSharedData )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "URI: %1 " ).arg( uri ), 2 );
|
|
|
|
mUri = QgsDataSourceUri( uri );
|
|
|
|
// populate members from the uri structure
|
|
mSchemaName = mUri.schema();
|
|
mTableName = mUri.table();
|
|
mGeometryColumn = mUri.geometryColumn();
|
|
mBoundingBoxColumn = mUri.param( "bbox" );
|
|
if ( mBoundingBoxColumn.isEmpty() )
|
|
{
|
|
mBoundingBoxColumn = mGeometryColumn;
|
|
}
|
|
mSqlWhereClause = mUri.sql();
|
|
mRequestedSrid = mUri.srid();
|
|
mRequestedGeomType = mUri.wkbType();
|
|
|
|
const QString checkUnicityKey { QStringLiteral( "checkPrimaryKeyUnicity" ) };
|
|
if ( mUri.hasParam( checkUnicityKey ) )
|
|
{
|
|
if ( mUri.param( checkUnicityKey ).compare( QLatin1String( "0" ) ) == 0 )
|
|
{
|
|
mCheckPrimaryKeyUnicity = false;
|
|
}
|
|
else
|
|
{
|
|
mCheckPrimaryKeyUnicity = true;
|
|
}
|
|
if ( mReadFlags & Qgis::DataProviderReadFlag::TrustDataSource )
|
|
{
|
|
mCheckPrimaryKeyUnicity = false;
|
|
}
|
|
}
|
|
|
|
if ( mSchemaName.isEmpty() && mTableName.startsWith( '(' ) && mTableName.endsWith( ')' ) )
|
|
{
|
|
mIsQuery = true;
|
|
setQuery( mTableName );
|
|
mTableName.clear();
|
|
}
|
|
else
|
|
{
|
|
mIsQuery = false;
|
|
|
|
setQuery( ( !mSchemaName.isEmpty() ? quotedIdentifier( mSchemaName ) + '.' : QString() ) + ( !mTableName.isEmpty() ? quotedIdentifier( mTableName ) : QString() ) );
|
|
}
|
|
|
|
mUseEstimatedMetadata = mUri.useEstimatedMetadata();
|
|
if ( mReadFlags & Qgis::DataProviderReadFlag::TrustDataSource )
|
|
{
|
|
mUseEstimatedMetadata = true;
|
|
}
|
|
mSelectAtIdDisabled = mUri.selectAtIdDisabled();
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Connection info is %1" ).arg( QgsPostgresConn::connectionInfo( mUri, false ) ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Geometry column is: %1" ).arg( mGeometryColumn ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Schema is: %1" ).arg( mSchemaName ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Table name is: %1" ).arg( mTableName ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Query is: %1" ).arg( mQuery ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Where clause is: %1" ).arg( mSqlWhereClause ), 2 );
|
|
|
|
// no table/query passed, the provider could be used to get tables
|
|
if ( mQuery.isEmpty() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
mConnectionRO = QgsPostgresConn::connectDb( mUri, true, true, false, !mReadFlags.testFlag( Qgis::DataProviderReadFlag::SkipCredentialsRequest ) );
|
|
if ( !mConnectionRO )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// if credentials were updated during database connection, update the provider's uri accordingly
|
|
if ( !mUri.username().isEmpty() )
|
|
mUri.setUsername( mConnectionRO->uri().username() );
|
|
if ( !mUri.password().isEmpty() )
|
|
mUri.setPassword( mConnectionRO->uri().password() );
|
|
|
|
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;
|
|
}
|
|
|
|
if ( mDetectedGeomType != Qgis::WkbType::Unknown )
|
|
{
|
|
// most common case, elevation can be computed via mDetectedGeomType
|
|
elevationProperties()->setContainsElevationData( QgsWkbTypes::hasZ( mDetectedGeomType ) );
|
|
}
|
|
|
|
// NOTE: mValid would be true after true return from
|
|
// getGeometryDetails, see https://github.com/qgis/QGIS/issues/21807
|
|
|
|
if ( mSpatialColType == SctTopoGeometry )
|
|
{
|
|
if ( !getTopoLayerInfo() ) // gets topology name and layer id
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Invalid PostgreSQL topology layer" ), tr( "PostGIS" ) );
|
|
mValid = false;
|
|
disconnectDb();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Try to load metadata
|
|
const QString schemaQuery = QStringLiteral( "SELECT table_schema FROM information_schema.tables WHERE table_name = 'qgis_layer_metadata'" );
|
|
QgsPostgresResult res( mConnectionRO->LoggedPQexec( "QgsPostgresProvider", schemaQuery ) );
|
|
if ( res.PQntuples() > 0 )
|
|
{
|
|
const QString schemaName = res.PQgetvalue( 0, 0 );
|
|
// TODO: also filter CRS?
|
|
const QString selectQuery = QStringLiteral( R"SQL(
|
|
SELECT
|
|
qmd
|
|
FROM %4.qgis_layer_metadata
|
|
WHERE
|
|
f_table_schema=%1
|
|
AND f_table_name=%2
|
|
AND f_geometry_column %3
|
|
AND layer_type='vector'
|
|
)SQL" )
|
|
.arg( QgsPostgresConn::quotedValue( mUri.schema() ) )
|
|
.arg( QgsPostgresConn::quotedValue( mUri.table() ) )
|
|
.arg( mUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( mUri.geometryColumn() ) ) )
|
|
.arg( QgsPostgresConn::quotedIdentifier( schemaName ) );
|
|
|
|
QgsPostgresResult res( mConnectionRO->LoggedPQexec( "QgsPostgresProvider", selectQuery ) );
|
|
if ( res.PQntuples() > 0 )
|
|
{
|
|
QgsLayerMetadata metadata;
|
|
QDomDocument doc;
|
|
doc.setContent( res.PQgetvalue( 0, 0 ) );
|
|
mLayerMetadata.readMetadataXml( doc.documentElement() );
|
|
}
|
|
}
|
|
|
|
// set the primary key
|
|
if ( !determinePrimaryKey() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "PostgreSQL layer has no primary key." ), tr( "PostGIS" ) );
|
|
mValid = false;
|
|
disconnectDb();
|
|
return;
|
|
}
|
|
|
|
setNativeTypes( mConnectionRO->nativeTypes() );
|
|
|
|
QString key;
|
|
switch ( mPrimaryKeyType )
|
|
{
|
|
case PktOid:
|
|
key = QStringLiteral( "oid" );
|
|
break;
|
|
case PktTid:
|
|
key = QStringLiteral( "tid" );
|
|
break;
|
|
case PktInt:
|
|
Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
|
|
Q_ASSERT( mPrimaryKeyAttrs[0] >= 0 && mPrimaryKeyAttrs[0] < mAttributeFields.count() );
|
|
key = mAttributeFields.at( mPrimaryKeyAttrs.at( 0 ) ).name();
|
|
break;
|
|
case PktInt64:
|
|
case PktUint64:
|
|
case PktFidMap:
|
|
{
|
|
QString delim;
|
|
const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
|
|
for ( int idx : constMPrimaryKeyAttrs )
|
|
{
|
|
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() );
|
|
|
|
// Constructor is called in another thread than the thread where the provider will live,
|
|
// so we disconnect the DB, connection will be done later in the provider thread when needed
|
|
if ( flags.testFlag( Qgis::DataProviderReadFlag::ParallelThreadLoading ) )
|
|
disconnectDb();
|
|
}
|
|
|
|
QgsPostgresProvider::~QgsPostgresProvider()
|
|
{
|
|
disconnectDb();
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "deconstructing." ), 3 );
|
|
}
|
|
|
|
|
|
QgsAbstractFeatureSource *QgsPostgresProvider::featureSource() const
|
|
{
|
|
return new QgsPostgresFeatureSource( this );
|
|
}
|
|
|
|
QgsPostgresConn *QgsPostgresProvider::connectionRO() const
|
|
{
|
|
if ( mTransaction )
|
|
return mTransaction->connection();
|
|
|
|
if ( !mConnectionRO )
|
|
mConnectionRO = QgsPostgresConn::connectDb( mUri, true, true, false, !mReadFlags.testFlag( Qgis::DataProviderReadFlag::SkipCredentialsRequest ) );
|
|
|
|
return mConnectionRO;
|
|
}
|
|
|
|
void QgsPostgresProvider::setListening( bool isListening )
|
|
{
|
|
if ( !mValid )
|
|
return;
|
|
|
|
if ( isListening && !mListener )
|
|
{
|
|
mListener = QgsPostgresListener::create( QgsPostgresConn::connectionInfo( mUri, false ) );
|
|
connect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify );
|
|
}
|
|
else if ( !isListening && mListener )
|
|
{
|
|
disconnect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify );
|
|
mListener.reset();
|
|
}
|
|
}
|
|
|
|
Qgis::VectorLayerTypeFlags QgsPostgresProvider::vectorLayerTypeFlags() const
|
|
{
|
|
Qgis::VectorLayerTypeFlags flags;
|
|
if ( mValid && mIsQuery )
|
|
{
|
|
flags.setFlag( Qgis::VectorLayerTypeFlag::SqlQuery );
|
|
}
|
|
return flags;
|
|
}
|
|
|
|
void QgsPostgresProvider::handlePostCloneOperations( QgsVectorDataProvider *source )
|
|
{
|
|
mShared = qobject_cast<QgsPostgresProvider *>( source )->mShared;
|
|
}
|
|
|
|
void QgsPostgresProvider::reloadProviderData()
|
|
{
|
|
mShared->setFeaturesCounted( -1 );
|
|
mLayerExtent.reset();
|
|
}
|
|
|
|
QgsPostgresConn *QgsPostgresProvider::connectionRW()
|
|
{
|
|
if ( mTransaction )
|
|
{
|
|
return mTransaction->connection();
|
|
}
|
|
else if ( !mConnectionRW )
|
|
{
|
|
mConnectionRW = QgsPostgresConn::connectDb( mUri, false );
|
|
}
|
|
return mConnectionRW;
|
|
}
|
|
|
|
QgsTransaction *QgsPostgresProvider::transaction() const
|
|
{
|
|
return mTransaction;
|
|
}
|
|
|
|
QString QgsPostgresProvider::providerKey()
|
|
{
|
|
return POSTGRES_KEY;
|
|
}
|
|
|
|
void QgsPostgresProvider::setTransaction( QgsTransaction *transaction )
|
|
{
|
|
// static_cast since layers cannot be added to a transaction of a non-matching provider
|
|
mTransaction = static_cast<QgsPostgresTransaction *>( transaction );
|
|
|
|
const QString sessionRoleKey = QStringLiteral( "session_role" );
|
|
if ( mUri.hasParam( sessionRoleKey ) )
|
|
{
|
|
const QString sessionRole = mUri.param( sessionRoleKey );
|
|
if ( !sessionRole.isEmpty() )
|
|
{
|
|
if ( !mTransaction->connection()->setSessionRole( sessionRole ) )
|
|
{
|
|
QgsDebugMsgLevel(
|
|
QStringLiteral(
|
|
"Set session role failed for ROLE %1"
|
|
)
|
|
.arg( quotedValue( sessionRole ) ),
|
|
2
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsReferencedGeometry QgsPostgresProvider::fromEwkt( const QString &ewkt, QgsPostgresConn *conn )
|
|
{
|
|
thread_local const QRegularExpression regularExpressionSRID( "^SRID=(\\d+);" );
|
|
|
|
QRegularExpressionMatch regularExpressionMatch = regularExpressionSRID.match( ewkt );
|
|
if ( !regularExpressionMatch.hasMatch() )
|
|
{
|
|
QgsGeometry geom = QgsGeometry::fromWkt( ewkt );
|
|
QgsCoordinateReferenceSystem crs; // TODO: use projects' crs ?
|
|
return QgsReferencedGeometry( geom, crs );
|
|
}
|
|
|
|
QString wkt = ewkt.mid( regularExpressionMatch.captured( 0 ).size() );
|
|
int srid = regularExpressionMatch.captured( 1 ).toInt();
|
|
|
|
|
|
QgsGeometry geom = QgsGeometry::fromWkt( wkt );
|
|
return QgsReferencedGeometry( geom, sridToCrs( srid, conn ) );
|
|
}
|
|
|
|
QString QgsPostgresProvider::toEwkt( const QgsReferencedGeometry &geom, QgsPostgresConn *conn )
|
|
{
|
|
if ( !geom.isNull() )
|
|
return QStringLiteral( "SRID=%1;%2" ).arg( QString::number( crsToSrid( geom.crs(), conn ) ), geom.asWkt() );
|
|
else
|
|
return QString();
|
|
}
|
|
|
|
QString QgsPostgresProvider::geomAttrToString( const QVariant &attr, QgsPostgresConn *conn )
|
|
{
|
|
if ( attr.userType() == QMetaType::Type::QString )
|
|
return attr.toString();
|
|
else
|
|
return toEwkt( attr.value<QgsReferencedGeometry>(), conn );
|
|
}
|
|
|
|
int QgsPostgresProvider::crsToSrid( const QgsCoordinateReferenceSystem &crs, QgsPostgresConn *conn )
|
|
{
|
|
int srid = -1;
|
|
if ( conn )
|
|
srid = conn->crsToSrid( crs );
|
|
return srid;
|
|
}
|
|
|
|
QgsCoordinateReferenceSystem QgsPostgresProvider::sridToCrs( int srid, QgsPostgresConn *conn )
|
|
{
|
|
QgsCoordinateReferenceSystem crs;
|
|
if ( conn )
|
|
crs = conn->sridToCrs( srid );
|
|
return crs;
|
|
}
|
|
|
|
void QgsPostgresProvider::disconnectDb()
|
|
{
|
|
if ( mConnectionRO )
|
|
{
|
|
mConnectionRO->unref();
|
|
mConnectionRO = nullptr;
|
|
}
|
|
|
|
if ( mConnectionRW )
|
|
{
|
|
mConnectionRW->unref();
|
|
mConnectionRW = nullptr;
|
|
}
|
|
}
|
|
|
|
QString QgsPostgresProvider::quotedByteaValue( const QVariant &value )
|
|
{
|
|
if ( QgsVariantUtils::isNull( value ) )
|
|
return QStringLiteral( "NULL" );
|
|
|
|
const QByteArray ba = value.toByteArray();
|
|
const unsigned char *buf = reinterpret_cast<const unsigned char *>( ba.constData() );
|
|
QString param;
|
|
param.reserve( ba.length() * 4 );
|
|
for ( int i = 0; i < ba.length(); ++i )
|
|
{
|
|
param += QStringLiteral( "\\%1" ).arg( static_cast<int>( buf[i] ), 3, 8, QChar( '0' ) );
|
|
}
|
|
return QStringLiteral( "decode('%1','escape')" ).arg( param );
|
|
}
|
|
|
|
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<QgsPostgresFeatureSource *>( 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:
|
|
Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
|
|
whereClause = QStringLiteral( "%3%1=$%2" ).arg( quotedIdentifier( field( mPrimaryKeyAttrs[0] ).name() ) ).arg( offset ).arg( aliased );
|
|
break;
|
|
|
|
case PktInt64:
|
|
case PktUint64:
|
|
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()->fieldExpressionForWhereClause( 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:
|
|
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 PktInt64:
|
|
case PktUint64:
|
|
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
|
|
{
|
|
QgsDebugError( QStringLiteral( "FAILURE: Key value %1 for feature %2 not found." ).arg( mPrimaryKeyAttrs[i] ).arg( featureId ) );
|
|
params << QStringLiteral( "NULL" );
|
|
}
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "keys params: %1" ).arg( params.join( "; " ) ), 2 );
|
|
}
|
|
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 QgsPostgresProvider::whereClause( QgsFeatureIds featureIds ) const
|
|
{
|
|
return QgsPostgresUtils::whereClause( featureIds, mAttributeFields, connectionRO(), mPrimaryKeyType, mPrimaryKeyAttrs, mShared );
|
|
}
|
|
|
|
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 != Qgis::WkbType::Unknown && mRequestedGeomType != mDetectedGeomType )
|
|
{
|
|
where += delim + QgsPostgresConn::postgisTypeFilter( mGeometryColumn, mRequestedGeomType, mSpatialColType == SctGeography );
|
|
delim = QStringLiteral( " AND " );
|
|
}
|
|
|
|
return where;
|
|
}
|
|
|
|
void QgsPostgresProvider::setExtent( const QgsRectangle &newExtent )
|
|
{
|
|
mLayerExtent.emplace( newExtent );
|
|
}
|
|
|
|
/**
|
|
* Returns the feature type
|
|
*/
|
|
Qgis::WkbType QgsPostgresProvider::wkbType() const
|
|
{
|
|
return mRequestedGeomType != Qgis::WkbType::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()
|
|
{
|
|
// Clear cached information about enum values support
|
|
mShared->clearSupportsEnumValuesCache();
|
|
|
|
QString sql;
|
|
QString attroidsFilter;
|
|
|
|
if ( !mIsQuery )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Loading fields for table %1" ).arg( mTableName ), 2 );
|
|
|
|
if ( mLayerMetadata.abstract().isEmpty() )
|
|
{
|
|
// Get the table description
|
|
sql = QStringLiteral( "SELECT description FROM pg_description WHERE objoid=regclass(%1)::oid AND objsubid=0" ).arg( quotedValue( mQuery ) );
|
|
QgsPostgresResult tresult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
if ( !tresult.result() )
|
|
{
|
|
throw PGException( tresult );
|
|
}
|
|
|
|
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)
|
|
sql = QStringLiteral( "SELECT * FROM %1 LIMIT 0" ).arg( mQuery );
|
|
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
QMap<Oid, QMap<int, QString>> fmtFieldTypeMap, descrMap, defValMap, identityMap, generatedMap;
|
|
QMap<Oid, QMap<int, Oid>> attTypeIdMap;
|
|
QMap<Oid, QMap<int, bool>> notNullMap, uniqueMap;
|
|
if ( result.PQnfields() > 0 )
|
|
{
|
|
// Collect attribute oids
|
|
QSet<Oid> attroids;
|
|
for ( int i = 0; i < result.PQnfields(); i++ )
|
|
{
|
|
Oid attroid = result.PQftype( i );
|
|
attroids.insert( attroid );
|
|
}
|
|
|
|
// Collect table oids
|
|
QSet<Oid> tableoids;
|
|
for ( int i = 0; i < result.PQnfields(); i++ )
|
|
{
|
|
Oid tableoid = result.PQftable( i );
|
|
if ( tableoid > 0 )
|
|
{
|
|
tableoids.insert( tableoid );
|
|
}
|
|
}
|
|
|
|
if ( !tableoids.isEmpty() )
|
|
{
|
|
QStringList tableoidsList;
|
|
const auto constTableoids = tableoids;
|
|
for ( Oid tableoid : constTableoids )
|
|
{
|
|
tableoidsList.append( QString::number( tableoid ) );
|
|
}
|
|
|
|
QString tableoidsFilter = '(' + tableoidsList.join( QLatin1Char( ',' ) ) + ')';
|
|
|
|
// Collect formatted field types
|
|
sql = QStringLiteral(
|
|
"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%1%2"
|
|
" 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 %3"
|
|
)
|
|
.arg( connectionRO()->pgVersion() >= 100000 ? QStringLiteral( ", attidentity" ) : QString(), connectionRO()->pgVersion() >= 120000 ? QStringLiteral( ", attgenerated" ) : QString(), tableoidsFilter );
|
|
|
|
QgsPostgresResult fmtFieldTypeResult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
if ( !fmtFieldTypeResult.result() )
|
|
{
|
|
throw PGException( fmtFieldTypeResult );
|
|
}
|
|
|
|
for ( int i = 0; i < fmtFieldTypeResult.PQntuples(); ++i )
|
|
{
|
|
Oid attrelid = fmtFieldTypeResult.PQgetvalue( i, 0 ).toUInt();
|
|
int attnum = fmtFieldTypeResult.PQgetvalue( i, 1 ).toInt(); // Int2
|
|
QString formatType = fmtFieldTypeResult.PQgetvalue( i, 2 );
|
|
QString descr = fmtFieldTypeResult.PQgetvalue( i, 3 );
|
|
QString defVal = fmtFieldTypeResult.PQgetvalue( i, 4 );
|
|
Oid attType = fmtFieldTypeResult.PQgetvalue( i, 5 ).toUInt();
|
|
bool attNotNull = fmtFieldTypeResult.PQgetvalue( i, 6 ).toInt();
|
|
bool uniqueConstraint = fmtFieldTypeResult.PQgetvalue( i, 7 ).toInt();
|
|
QString attIdentity = connectionRO()->pgVersion() >= 100000 ? fmtFieldTypeResult.PQgetvalue( i, 8 ) : " ";
|
|
|
|
// On PostgreSQL 12, the field pg_attribute.attgenerated is always filled with "s" if the field is generated,
|
|
// with the possibility of other values in future releases. This indicates "STORED" generated fields.
|
|
// The documentation for version 12 indicates that there is a future possibility of supporting virtual
|
|
// generated values, which might make possible to have values other than "s" on pg_attribute.attgenerated,
|
|
// which should be unimportant for QGIS if the user still won't be able to overwrite the column value.
|
|
// See https://www.postgresql.org/docs/12/ddl-generated-columns.html
|
|
QString attGenerated = connectionRO()->pgVersion() >= 120000 ? fmtFieldTypeResult.PQgetvalue( i, 9 ) : "";
|
|
fmtFieldTypeMap[attrelid][attnum] = formatType;
|
|
descrMap[attrelid][attnum] = descr;
|
|
defValMap[attrelid][attnum] = defVal;
|
|
attTypeIdMap[attrelid][attnum] = attType;
|
|
notNullMap[attrelid][attnum] = attNotNull;
|
|
uniqueMap[attrelid][attnum] = uniqueConstraint;
|
|
identityMap[attrelid][attnum] = attIdentity.isEmpty() ? " " : attIdentity;
|
|
generatedMap[attrelid][attnum] = attGenerated.isEmpty() ? QString() : defVal;
|
|
|
|
// Also include atttype oid from pg_attribute, because PQnfields only returns basic type for for domains
|
|
attroids.insert( attType );
|
|
}
|
|
}
|
|
|
|
// Prepare filter for fetching pg_type info
|
|
if ( !attroids.isEmpty() )
|
|
{
|
|
QStringList attroidsList;
|
|
for ( Oid attroid : std::as_const( attroids ) )
|
|
{
|
|
attroidsList.append( QString::number( attroid ) );
|
|
}
|
|
attroidsFilter = QStringLiteral( "WHERE oid in (%1)" ).arg( attroidsList.join( ',' ) );
|
|
}
|
|
}
|
|
|
|
// Collect type info
|
|
sql = QStringLiteral( "SELECT oid,typname,typtype,typelem,typlen FROM pg_type %1" ).arg( attroidsFilter );
|
|
QgsPostgresResult typeResult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
QMap<Oid, PGTypeInfo> 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 ).toUInt(), typeInfo );
|
|
}
|
|
|
|
QSet<QString> fields;
|
|
mAttributeFields.clear();
|
|
mIdentityFields.clear();
|
|
for ( int i = 0; i < result.PQnfields(); i++ )
|
|
{
|
|
QString fieldName = result.PQfname( i );
|
|
Oid tableoid = result.PQftable( i );
|
|
int attnum = result.PQftablecol( i );
|
|
QString formattedFieldType = fmtFieldTypeMap[tableoid][attnum];
|
|
if ( fieldName == mGeometryColumn )
|
|
{
|
|
if ( mDetectedGeomType == Qgis::WkbType::Unknown )
|
|
{
|
|
// rare case, elevation can not be computed via mDetectedGeomType, we parse the formattedFieldType and look for Z data
|
|
const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( "geometry\\(\\w+\\s*ZM?(,\\d+)?\\)" ), QRegularExpression::CaseInsensitiveOption );
|
|
const QRegularExpressionMatch match = re.match( formattedFieldType );
|
|
elevationProperties()->setContainsElevationData( match.hasMatch() );
|
|
}
|
|
continue;
|
|
}
|
|
|
|
Oid fldtyp = result.PQftype( i );
|
|
int fldMod = result.PQfmod( i );
|
|
int fieldPrec = 0;
|
|
Oid 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 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()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( fmtFieldModResult.PQntuples() > 0 )
|
|
{
|
|
formattedFieldType = fmtFieldModResult.PQgetvalue( 0, 0 );
|
|
}
|
|
}
|
|
|
|
QString fieldComment = descrMap[tableoid][attnum];
|
|
|
|
QMetaType::Type fieldType = QMetaType::Type::UnknownType;
|
|
QMetaType::Type fieldSubType = QMetaType::Type::UnknownType;
|
|
|
|
if ( fieldTType == QLatin1String( "b" ) )
|
|
{
|
|
bool isArray = fieldTypeName.startsWith( '_' );
|
|
|
|
if ( isArray )
|
|
fieldTypeName = fieldTypeName.mid( 1 );
|
|
|
|
if ( fieldTypeName == QLatin1String( "int8" ) || fieldTypeName == QLatin1String( "serial8" ) )
|
|
{
|
|
fieldType = QMetaType::Type::LongLong;
|
|
fieldSize = -1;
|
|
fieldPrec = 0;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "int2" ) || fieldTypeName == QLatin1String( "int4" ) || fieldTypeName == QLatin1String( "oid" ) || fieldTypeName == QLatin1String( "serial" ) )
|
|
{
|
|
fieldType = QMetaType::Type::Int;
|
|
fieldSize = -1;
|
|
fieldPrec = 0;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "real" ) || fieldTypeName == QLatin1String( "double precision" ) || fieldTypeName == QLatin1String( "float4" ) || fieldTypeName == QLatin1String( "float8" ) )
|
|
{
|
|
fieldType = QMetaType::Type::Double;
|
|
fieldSize = -1;
|
|
fieldPrec = 0;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "numeric" ) )
|
|
{
|
|
fieldType = QMetaType::Type::Double;
|
|
|
|
if ( formattedFieldType == QLatin1String( "numeric" ) || formattedFieldType.isEmpty() )
|
|
{
|
|
fieldSize = -1;
|
|
fieldPrec = 0;
|
|
}
|
|
else
|
|
{
|
|
const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( QStringLiteral( "numeric\\((\\d+),(\\d+)\\)" ) ) );
|
|
const QRegularExpressionMatch match = re.match( formattedFieldType );
|
|
if ( match.hasMatch() )
|
|
{
|
|
fieldSize = match.captured( 1 ).toInt();
|
|
fieldPrec = match.captured( 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 = 0;
|
|
}
|
|
}
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "money" ) )
|
|
{
|
|
fieldType = QMetaType::Type::Double;
|
|
fieldSize = -1;
|
|
fieldPrec = 2;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "varchar" ) )
|
|
{
|
|
fieldType = QMetaType::Type::QString;
|
|
|
|
const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( "character varying\\((\\d+)\\)" ) );
|
|
const QRegularExpressionMatch match = re.match( formattedFieldType );
|
|
if ( match.hasMatch() )
|
|
{
|
|
fieldSize = match.captured( 1 ).toInt();
|
|
}
|
|
else
|
|
{
|
|
fieldSize = -1;
|
|
}
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "date" ) )
|
|
{
|
|
fieldType = QMetaType::Type::QDate;
|
|
fieldSize = -1;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "time" ) )
|
|
{
|
|
fieldType = QMetaType::Type::QTime;
|
|
fieldSize = -1;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "timestamp" ) || fieldTypeName == QLatin1String( "timestamptz" ) )
|
|
{
|
|
fieldType = QMetaType::Type::QDateTime;
|
|
fieldSize = -1;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "bytea" ) )
|
|
{
|
|
fieldType = QMetaType::Type::QByteArray;
|
|
fieldSize = -1;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "text" ) || fieldTypeName == QLatin1String( "citext" ) || fieldTypeName == QLatin1String( "geography" ) || fieldTypeName == QLatin1String( "inet" ) || fieldTypeName == QLatin1String( "cidr" ) || fieldTypeName == QLatin1String( "macaddr" ) || fieldTypeName == QLatin1String( "macaddr8" ) || fieldTypeName == QLatin1String( "ltree" ) || fieldTypeName == QLatin1String( "uuid" ) || fieldTypeName == QLatin1String( "xml" ) || fieldTypeName == QLatin1String( "bit" ) || fieldTypeName == QLatin1String( "varbit" ) || fieldTypeName.startsWith( QLatin1String( "time" ) ) || fieldTypeName.startsWith( QLatin1String( "date" ) ) )
|
|
{
|
|
fieldType = QMetaType::Type::QString;
|
|
fieldSize = -1;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "geometry" ) )
|
|
{
|
|
fieldType = QMetaType::Type::User;
|
|
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 = QMetaType::Type::QString;
|
|
|
|
const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( "character\\((\\d+)\\)" ) );
|
|
const QRegularExpressionMatch match = re.match( formattedFieldType );
|
|
if ( match.hasMatch() )
|
|
{
|
|
fieldSize = match.captured( 1 ).toInt();
|
|
}
|
|
else
|
|
{
|
|
QgsDebugError( QStringLiteral( "Unexpected formatted field type '%1' for field %2" )
|
|
.arg( formattedFieldType, fieldName ) );
|
|
fieldSize = -1;
|
|
fieldPrec = 0;
|
|
}
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "char" ) )
|
|
{
|
|
fieldType = QMetaType::Type::QString;
|
|
|
|
const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( QStringLiteral( "char\\((\\d+)\\)" ) ) );
|
|
const QRegularExpressionMatch match = re.match( formattedFieldType );
|
|
if ( match.hasMatch() )
|
|
{
|
|
fieldSize = match.captured( 1 ).toInt();
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Unexpected formatted field type '%1' for field %2" )
|
|
.arg( formattedFieldType, fieldName ) );
|
|
fieldSize = -1;
|
|
fieldPrec = 0;
|
|
}
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "hstore" ) || fieldTypeName == QLatin1String( "json" ) || fieldTypeName == QLatin1String( "jsonb" ) )
|
|
{
|
|
fieldType = QMetaType::Type::QVariantMap;
|
|
fieldSubType = QMetaType::Type::QString;
|
|
fieldSize = -1;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "bool" ) )
|
|
{
|
|
// enum
|
|
fieldType = QMetaType::Type::Bool;
|
|
fieldSize = -1;
|
|
}
|
|
// PG 12 returns "name" type for some system table fields (e.g. information_schema.tables)
|
|
else if ( fieldTypeName == QLatin1String( "name" ) )
|
|
{
|
|
fieldType = QMetaType::Type::QString;
|
|
fieldSize = 63;
|
|
}
|
|
else
|
|
{
|
|
// be tolerant in case of views: this might be a field used as a key
|
|
const Qgis::PostgresRelKind type = relkind();
|
|
if ( ( type == Qgis::PostgresRelKind::View || type == Qgis::PostgresRelKind::MaterializedView ) && parseUriKey( mUri.keyColumn() ).contains( fieldName ) )
|
|
{
|
|
// Assume it is convertible to text
|
|
fieldType = QMetaType::Type::QString;
|
|
fieldSize = -1;
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "unknown" ) )
|
|
{
|
|
// Assume it is convertible to text
|
|
fieldType = QMetaType::Type::QString;
|
|
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 == QMetaType::Type::QString ? QMetaType::Type::QStringList : QMetaType::Type::QVariantList );
|
|
fieldSize = -1;
|
|
}
|
|
}
|
|
else if ( fieldTType == QLatin1String( "e" ) )
|
|
{
|
|
// enum
|
|
fieldType = QMetaType::Type::QString;
|
|
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" ) );
|
|
// In case of read-only query layers we can safely ignore the issue and rename the duplicated field
|
|
if ( !mIsQuery )
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
unsigned short int i = 1;
|
|
while ( i < std::numeric_limits<unsigned short int>::max() )
|
|
{
|
|
const QString newName { QStringLiteral( "%1 (%2)" ).arg( fieldName ).arg( ++i ) };
|
|
if ( !fields.contains( newName ) )
|
|
{
|
|
fieldName = newName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fields << fieldName;
|
|
|
|
if ( isDomain )
|
|
{
|
|
//field was defined using domain, so use domain type name for fieldTypeName
|
|
fieldTypeName = originalFormattedFieldType;
|
|
}
|
|
|
|
// If this is an identity field with constraints and there is no default, let's look for a sequence:
|
|
// we might have a default value created by a sequence named <table>_<field>_seq
|
|
if ( !identityMap[tableoid][attnum].isEmpty()
|
|
&& notNullMap[tableoid][attnum]
|
|
&& uniqueMap[tableoid][attnum]
|
|
&& defValMap[tableoid][attnum].isEmpty() )
|
|
{
|
|
const QString seqSql = QStringLiteral( "SELECT pg_get_serial_sequence(%1, %2)" )
|
|
.arg( quotedValue( mQuery ) )
|
|
.arg( quotedValue( fieldName ) );
|
|
QgsPostgresResult seqResult( connectionRO()->PQexec( seqSql ) );
|
|
if ( seqResult.PQntuples() == 1 && !seqResult.PQgetisnull( 0, 0 ) )
|
|
{
|
|
defValMap[tableoid][attnum] = QStringLiteral( "nextval(%1)" ).arg( quotedValue( seqResult.PQgetvalue( 0, 0 ) ) );
|
|
}
|
|
}
|
|
|
|
mDefaultValues.insert( mAttributeFields.size(), defValMap[tableoid][attnum] );
|
|
|
|
const QString generatedValue = generatedMap[tableoid][attnum];
|
|
if ( !generatedValue.isNull() )
|
|
mGeneratedValues.insert( mAttributeFields.size(), generatedValue );
|
|
|
|
QgsField newField = QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment, fieldSubType );
|
|
newField.setReadOnly( !generatedValue.isNull() );
|
|
|
|
QgsFieldConstraints constraints;
|
|
if ( notNullMap[tableoid][attnum] || ( mPrimaryKeyAttrs.size() == 1 && mPrimaryKeyAttrs[0] == i ) || identityMap[tableoid][attnum] != ' ' )
|
|
constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
|
|
if ( uniqueMap[tableoid][attnum] || ( mPrimaryKeyAttrs.size() == 1 && mPrimaryKeyAttrs[0] == i ) || identityMap[tableoid][attnum] != ' ' )
|
|
constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
|
|
newField.setConstraints( constraints );
|
|
|
|
const QString identity = identityMap[tableoid][attnum];
|
|
if ( !identity.isEmpty() )
|
|
{
|
|
mIdentityFields.insert( mAttributeFields.size(), identity[0].toLatin1() );
|
|
}
|
|
mAttributeFields.append( newField );
|
|
|
|
// if we know for sure that this field is not enumerated type or a domain type, let's
|
|
// mark it here, so that enumValues() does not need to check it again (for types like int, text, ...)
|
|
if ( fieldTType != QLatin1String( "e" ) && !isDomain )
|
|
mShared->setFieldSupportsEnumValues( fields.count() - 1, false );
|
|
}
|
|
|
|
setEditorWidgets();
|
|
|
|
return true;
|
|
}
|
|
|
|
void QgsPostgresProvider::setEditorWidgets()
|
|
{
|
|
if ( !QgsPostgresUtils::tableExists( connectionRO(), EDITOR_WIDGET_STYLES_TABLE ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
QStringList quotedFnames;
|
|
const QStringList fieldNames = mAttributeFields.names();
|
|
for ( const QString &name : fieldNames )
|
|
{
|
|
quotedFnames << quotedValue( name );
|
|
}
|
|
|
|
// We expect the table to be created like this:
|
|
//
|
|
// 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));
|
|
const QString sql = QStringLiteral( "SELECT field_name, type, config "
|
|
"FROM %1 WHERE schema_name = %2 "
|
|
"AND table_name = %3 "
|
|
"AND field_name IN ( %4 )" )
|
|
.arg( EDITOR_WIDGET_STYLES_TABLE, quotedValue( mSchemaName ), quotedValue( mTableName ), quotedFnames.join( "," ) );
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
for ( int i = 0; i < result.PQntuples(); ++i )
|
|
{
|
|
if ( result.PQgetisnull( i, 2 ) )
|
|
continue; // config can be null and it's OK
|
|
|
|
const QString &configTxt = result.PQgetvalue( i, 2 );
|
|
const QString &type = result.PQgetvalue( i, 1 );
|
|
const QString &fname = result.PQgetvalue( i, 0 );
|
|
QVariantMap config;
|
|
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, fname ),
|
|
tr( "PostGIS" )
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Set corresponding editor widget
|
|
for ( auto &field : mAttributeFields )
|
|
{
|
|
if ( field.name() == fname )
|
|
{
|
|
field.setEditorWidgetSetup( QgsEditorWidgetSetup( type, config ) );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool QgsPostgresProvider::hasSufficientPermsAndCapabilities()
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Checking for permissions on the relation" ), 2 );
|
|
|
|
mEnabledCapabilities = Qgis::VectorProviderCapability::ReloadData;
|
|
|
|
QgsPostgresResult testAccess;
|
|
|
|
bool forceReadOnly = ( mReadFlags & Qgis::DataProviderReadFlag::ForceReadOnly );
|
|
bool inRecovery = false;
|
|
|
|
if ( !mIsQuery )
|
|
{
|
|
// 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 |= Qgis::VectorProviderCapability::SelectAtId;
|
|
}
|
|
|
|
QString sql = QStringLiteral(
|
|
"SELECT "
|
|
"has_table_privilege(%1,'SELECT')," // 0 (select priv)
|
|
"pg_is_in_recovery()," // 1 (in recovery)
|
|
"current_schema() " // 2 (current schema)
|
|
)
|
|
.arg( quotedValue( mQuery ) );
|
|
|
|
|
|
if ( connectionRO()->pgVersion() >= 80400 )
|
|
{
|
|
sql += QString(
|
|
",has_any_column_privilege(%1,'INSERT')" // 3 (insert priv)
|
|
",has_table_privilege(%1,'DELETE')" // 4 (delete priv)
|
|
",has_any_column_privilege(%1,'UPDATE')" // 5 (update priv)
|
|
",%2" // 6 (geom upd priv)
|
|
)
|
|
.arg( quotedValue( mQuery ), mGeometryColumn.isNull() ? QStringLiteral( "'f'" ) : QStringLiteral( "has_column_privilege(%1,%2,'UPDATE')" ).arg( quotedValue( mQuery ), quotedValue( mGeometryColumn ) ) );
|
|
}
|
|
else
|
|
{
|
|
sql += QString(
|
|
",has_table_privilege(%1,'INSERT')" // 3 (insert priv)
|
|
",has_table_privilege(%1,'DELETE')" // 4 (delete priv)
|
|
",has_table_privilege(%1,'UPDATE')" // 5 (update priv)
|
|
",has_table_privilege(%1,'UPDATE')" // 6 (geom col priv)
|
|
)
|
|
.arg( quotedValue( mQuery ) );
|
|
}
|
|
|
|
testAccess = connectionRO()->LoggedPQexec( "QgsPostgresProvider", 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" ) )
|
|
{
|
|
// SELECT
|
|
QgsMessageLog::logMessage( tr( "User has no SELECT privilege on %1 relation." ).arg( mQuery ), tr( "PostGIS" ) );
|
|
return false;
|
|
}
|
|
|
|
if ( testAccess.PQgetvalue( 0, 1 ) == QLatin1String( "t" ) )
|
|
{
|
|
// RECOVERY
|
|
QgsMessageLog::logMessage(
|
|
tr( "PostgreSQL is still in recovery after a database crash\n(or you are connected to a (read-only) standby server).\nWrite accesses will be denied." ),
|
|
tr( "PostGIS" )
|
|
);
|
|
inRecovery = true;
|
|
}
|
|
|
|
// CURRENT SCHEMA
|
|
if ( mSchemaName.isEmpty() )
|
|
mSchemaName = testAccess.PQgetvalue( 0, 2 );
|
|
|
|
// Do not set editable capabilities if the provider has been forced to be
|
|
// in read-only mode or if the database is still in recovery
|
|
if ( !forceReadOnly && !inRecovery )
|
|
{
|
|
if ( testAccess.PQgetvalue( 0, 3 ) == QLatin1String( "t" ) )
|
|
{
|
|
// INSERT
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::AddFeatures;
|
|
}
|
|
|
|
if ( testAccess.PQgetvalue( 0, 4 ) == QLatin1String( "t" ) )
|
|
{
|
|
// DELETE
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::FastTruncate;
|
|
}
|
|
|
|
if ( testAccess.PQgetvalue( 0, 5 ) == QLatin1String( "t" ) )
|
|
{
|
|
// UPDATE
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::ChangeAttributeValues;
|
|
}
|
|
|
|
if ( testAccess.PQgetvalue( 0, 6 ) == QLatin1String( "t" ) )
|
|
{
|
|
// UPDATE (geom column specific)
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::ChangeGeometries;
|
|
}
|
|
|
|
// TODO: merge this with the previous query
|
|
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()->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
if ( testAccess.PQresultStatus() == PGRES_TUPLES_OK && testAccess.PQntuples() == 1 )
|
|
{
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::AddAttributes | Qgis::VectorProviderCapability::DeleteAttributes | Qgis::VectorProviderCapability::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;
|
|
QRegularExpression regex;
|
|
do
|
|
{
|
|
alias = QStringLiteral( "subQuery_%1" ).arg( QString::number( index++ ) );
|
|
QString pattern = QStringLiteral( "(\\\"?)%1\\1" ).arg( QgsStringUtils::qRegExpEscape( alias ) );
|
|
regex.setPattern( pattern );
|
|
regex.setPatternOptions( QRegularExpression::CaseInsensitiveOption );
|
|
} while ( mQuery.contains( regex ) );
|
|
|
|
// convert the custom query into a subquery
|
|
setQuery( QStringLiteral( "%1 AS %2" )
|
|
.arg( mQuery, quotedIdentifier( alias ) ) );
|
|
|
|
QString sql = QStringLiteral( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery );
|
|
|
|
testAccess = connectionRO()->LoggedPQexec( "QgsPostgresProvider", 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 = Qgis::VectorProviderCapability::SelectAtId;
|
|
}
|
|
}
|
|
|
|
// supports geometry simplification on provider side
|
|
mEnabledCapabilities |= ( Qgis::VectorProviderCapability::SimplifyGeometries | Qgis::VectorProviderCapability::SimplifyGeometriesWithTopologicalValidation );
|
|
|
|
//supports transactions
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::TransactionSupport;
|
|
|
|
// supports circular geometries
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::CircularGeometries;
|
|
|
|
// supports layer metadata
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::ReadLayerMetadata;
|
|
|
|
if ( ( mEnabledCapabilities & Qgis::VectorProviderCapability::ChangeGeometries ) && ( mEnabledCapabilities & Qgis::VectorProviderCapability::ChangeAttributeValues ) && mSpatialColType != SctTopoGeometry )
|
|
{
|
|
mEnabledCapabilities |= Qgis::VectorProviderCapability::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 ) );
|
|
QgsDebugMsgLevel( QStringLiteral( "Checking whether %1 is a parent table" ).arg( sql ), 2 );
|
|
QgsPostgresResult res( connectionRO()->LoggedPQexec( "QgsPostgresProvider", 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 ) );
|
|
QgsDebugMsgLevel( QStringLiteral( "Retrieving first primary or unique index: %1" ).arg( sql ), 2 );
|
|
|
|
res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
QgsDebugMsgLevel( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ), 2 );
|
|
|
|
// no primary or unique indices found
|
|
if ( res.PQntuples() == 0 )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Relation has no primary key -- investigating alternatives" ), 2 );
|
|
|
|
// 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.
|
|
|
|
const Qgis::PostgresRelKind type = relkind();
|
|
|
|
if ( type == Qgis::PostgresRelKind::OrdinaryTable || type == Qgis::PostgresRelKind::PartitionedTable )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Relation is a table. Checking to see if it has an oid column." ), 2 );
|
|
|
|
mPrimaryKeyAttrs.clear();
|
|
mPrimaryKeyType = PktUnknown;
|
|
|
|
if ( connectionRO()->pgVersion() >= 100000 )
|
|
{
|
|
// If there is an generated id on the table, use that instead,
|
|
sql = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attidentity IN ('a','d') AND attrelid=regclass(%1) LIMIT 1" ).arg( quotedValue( mQuery ) );
|
|
res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
if ( res.PQntuples() == 1 )
|
|
{
|
|
// Could warn the user here that performance will suffer if
|
|
// attribute isn't indexed (and that they may want to add a
|
|
// primary key to the table)
|
|
int idx = fieldNameIndex( res.PQgetvalue( 0, 0 ) );
|
|
mPrimaryKeyType = pkType( mAttributeFields.at( idx ) );
|
|
mPrimaryKeyAttrs << idx;
|
|
}
|
|
}
|
|
|
|
if ( mPrimaryKeyType == PktUnknown )
|
|
{
|
|
// 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()->LoggedPQexec( "QgsPostgresProvider", 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;
|
|
}
|
|
}
|
|
|
|
if ( mPrimaryKeyType == PktUnknown )
|
|
{
|
|
sql = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attname='ctid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) );
|
|
|
|
res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", 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 &= ~( Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::ChangeAttributeValues | Qgis::VectorProviderCapability::ChangeGeometries | Qgis::VectorProviderCapability::ChangeFeatures );
|
|
}
|
|
}
|
|
|
|
if ( mPrimaryKeyType == PktUnknown )
|
|
{
|
|
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 == Qgis::PostgresRelKind::View || type == Qgis::PostgresRelKind::MaterializedView || type == Qgis::PostgresRelKind::ForeignTable )
|
|
{
|
|
determinePrimaryKeyFromUriKeyColumn();
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Unexpected relation type '%1'." ).arg( qgsEnumValueToKey( type ) ), 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 );
|
|
|
|
QgsDebugMsgLevel( "Retrieving key columns: " + sql, 2 );
|
|
res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
QgsDebugMsgLevel( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ), 2 );
|
|
|
|
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 )
|
|
{
|
|
QgsDebugMsgLevel( "Skipping " + name, 2 );
|
|
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();
|
|
}
|
|
|
|
if ( mPrimaryKeyAttrs.size() == 1 )
|
|
{
|
|
//primary keys are unique, not null
|
|
QgsFieldConstraints constraints = mAttributeFields.at( mPrimaryKeyAttrs[0] ).constraints();
|
|
constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
|
|
constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
|
|
mAttributeFields[mPrimaryKeyAttrs[0]].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() )
|
|
{
|
|
const QStringList cols = parseUriKey( primaryKey );
|
|
|
|
primaryKey.clear();
|
|
QString del;
|
|
for ( const QString &col : cols )
|
|
{
|
|
primaryKey += del + quotedIdentifier( col );
|
|
del = QStringLiteral( "," );
|
|
}
|
|
|
|
for ( 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( mPrimaryKeyAttrs.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()->LoggedPQexec( "QgsPostgresProvider", 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()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
return convertValue( fld.type(), fld.subType(), rmin.PQgetvalue( 0, 0 ), fld.typeName() );
|
|
}
|
|
catch ( PGFieldNotFound )
|
|
{
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
// Returns the list of unique values of an attribute
|
|
QSet<QVariant> QgsPostgresProvider::uniqueValues( int index, int limit ) const
|
|
{
|
|
QSet<QVariant> 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()->LoggedPQexec( "QgsPostgresProvider", 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()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), 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
|
|
{
|
|
if ( index < 0 || index >= mAttributeFields.count() )
|
|
return;
|
|
|
|
if ( !mShared->fieldSupportsEnumValuesIsSet( index ) )
|
|
{
|
|
mShared->setFieldSupportsEnumValues( index, true );
|
|
}
|
|
else if ( !mShared->fieldSupportsEnumValues( index ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//find out type of index
|
|
const QString fieldName = mAttributeFields.at( index ).name();
|
|
QString typeName = mAttributeFields.at( index ).typeName();
|
|
|
|
// Remove schema extension from typeName
|
|
typeName.remove( QRegularExpression( "^([^.]+\\.)+" ) );
|
|
|
|
//is type an enum?
|
|
const QString typeSql = QStringLiteral( "SELECT typtype FROM pg_type WHERE typname=%1" ).arg( quotedValue( typeName ) );
|
|
QgsPostgresResult typeRes( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), typeSql ) );
|
|
if ( typeRes.PQresultStatus() != PGRES_TUPLES_OK || typeRes.PQntuples() < 1 )
|
|
{
|
|
mShared->setFieldSupportsEnumValues( index, false );
|
|
return;
|
|
}
|
|
|
|
const 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 ) )
|
|
{
|
|
mShared->setFieldSupportsEnumValues( index, false );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//is there a domain check constraint for the attribute?
|
|
if ( !parseDomainCheckConstraint( enumList, fieldName ) )
|
|
{
|
|
mShared->setFieldSupportsEnumValues( index, false );
|
|
}
|
|
}
|
|
}
|
|
|
|
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) ORDER BY enumsortorder" )
|
|
.arg( quotedValue( mQuery ), quotedValue( attributeName ) );
|
|
QgsPostgresResult enumRangeRes( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), 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()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), domainSql ) );
|
|
if ( domainResult.PQresultStatus() == PGRES_TUPLES_OK && domainResult.PQntuples() > 0 && !domainResult.PQgetvalue( 0, 0 ).isNull() )
|
|
{
|
|
QString domainCheckDefinitionSql;
|
|
if ( connectionRO()->pgVersion() < 120000 )
|
|
{
|
|
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 ) ) );
|
|
}
|
|
else
|
|
{
|
|
domainCheckDefinitionSql = QStringLiteral( ""
|
|
"SELECT pg_catalog.pg_get_constraintdef( ( "
|
|
" SELECT oid FROM pg_constraint WHERE contypid = ( "
|
|
" SELECT oid FROM pg_type "
|
|
" WHERE typname = %1 "
|
|
" AND typnamespace =("
|
|
" SELECT oid FROM pg_namespace WHERE nspname = %2"
|
|
" )"
|
|
" )"
|
|
" ), true );" )
|
|
.arg( quotedValue( domainResult.PQgetvalue( 0, 0 ) ) )
|
|
.arg( quotedValue( domainResult.PQgetvalue( 0, 1 ) ) );
|
|
}
|
|
|
|
QgsPostgresResult domainCheckRes( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), 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 constraint has been specified as 'VALUE in ('a', 'b', 'c', 'd')
|
|
const thread_local QRegularExpression definitionRegExp( "VALUE\\s*=\\s*ANY\\s*\\(\\s*ARRAY\\s*\\[" );
|
|
int anyPos = checkDefinition.indexOf( definitionRegExp );
|
|
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 );
|
|
const QStringList commaSeparation = valueList.split( ',', Qt::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()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
return convertValue( fld.type(), fld.subType(), rmax.PQgetvalue( 0, 0 ), fld.typeName() );
|
|
}
|
|
catch ( PGFieldNotFound )
|
|
{
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
|
|
bool QgsPostgresProvider::isValid() const
|
|
{
|
|
return mValid;
|
|
}
|
|
|
|
Qgis::ProviderStyleStorageCapabilities QgsPostgresProvider::styleStorageCapabilities() const
|
|
{
|
|
Qgis::ProviderStyleStorageCapabilities storageCapabilities;
|
|
if ( isValid() )
|
|
{
|
|
storageCapabilities |= Qgis::ProviderStyleStorageCapability::SaveToDatabase;
|
|
storageCapabilities |= Qgis::ProviderStyleStorageCapability::LoadFromDatabase;
|
|
storageCapabilities |= Qgis::ProviderStyleStorageCapability::DeleteFromDatabase;
|
|
}
|
|
return storageCapabilities;
|
|
}
|
|
|
|
QString QgsPostgresProvider::defaultValueClause( int fieldId ) const
|
|
{
|
|
QString defVal = mDefaultValues.value( fieldId, QString() );
|
|
|
|
// with generated columns (PostgreSQL 12+), the provider will ALWAYS evaluate the default values.
|
|
// The only acceptable value for such columns on INSERT or UPDATE clauses is the keyword "DEFAULT".
|
|
// Here, we return the expression used to generate the field value, so the
|
|
// user can see what is happening when inserting a new feature.
|
|
// On inserting a new feature or updating a generated field, this is
|
|
// omitted from the generated queries.
|
|
// See https://www.postgresql.org/docs/12/ddl-generated-columns.html
|
|
if ( mGeneratedValues.contains( fieldId ) )
|
|
{
|
|
return defVal;
|
|
}
|
|
|
|
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()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), 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 ).isEmpty() && ( mDefaultValues.value( fieldIndex ) == value.toString() || QgsVariantUtils::isUnsetAttributeValue( value ) ) && !QgsVariantUtils::isNull( value );
|
|
}
|
|
}
|
|
|
|
QString QgsPostgresProvider::paramValue( const QString &fieldValue, const QString &defaultValue ) const
|
|
{
|
|
if ( fieldValue.isNull() )
|
|
return QString();
|
|
|
|
if ( fieldValue == defaultValue && !defaultValue.isNull() )
|
|
{
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), 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 = QStringLiteral( R"SQL(
|
|
SELECT
|
|
t.name,
|
|
l.layer_id,
|
|
l.level,
|
|
l.feature_type
|
|
FROM topology.layer l
|
|
JOIN topology.topology t ON (
|
|
l.topology_id = t.id
|
|
)
|
|
WHERE l.schema_name=%1
|
|
AND l.table_name=%2 AND l.feature_column=%3
|
|
)SQL" )
|
|
.arg( quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( mGeometryColumn ) );
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
throw PGException( result ); // we should probably not do this
|
|
}
|
|
|
|
if ( !result.PQntuples() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "TopoGeometry column %1.%2.%3 is not registered in topology.layer" ).arg( quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( mGeometryColumn ) ), tr( "PostGIS" ) );
|
|
|
|
QString dataQuery;
|
|
if ( mIsQuery )
|
|
{
|
|
dataQuery = QStringLiteral( "SELECT DISTINCT topology_id(%1), layer_id(%1) FROM %2 WHERE %1 IS NOT NULL" )
|
|
.arg( quotedIdentifier( mGeometryColumn ), mQuery );
|
|
}
|
|
else
|
|
{
|
|
dataQuery = QStringLiteral( "SELECT DISTINCT topology_id(%1), layer_id(%1) FROM %2.%3 WHERE %1 IS NOT NULL" )
|
|
.arg( quotedIdentifier( mGeometryColumn ), quotedIdentifier( mSchemaName ), quotedIdentifier( mTableName ) );
|
|
}
|
|
|
|
sql = QStringLiteral( R"SQL(
|
|
SELECT t.name, l.layer_id, l.level, l.feature_type
|
|
FROM
|
|
topology.topology t,
|
|
topology.layer l,
|
|
(
|
|
%1
|
|
) d
|
|
WHERE d.topology_id = t.id
|
|
AND l.topology_id = t.id
|
|
AND l.layer_id = d.layer_id
|
|
)SQL" )
|
|
.arg( dataQuery );
|
|
|
|
result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
if ( result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
throw PGException( result ); // we should probably not do this
|
|
}
|
|
if ( result.PQntuples() < 1 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Cannot find topology and layer information from data in %1.%2.%3" ).arg( quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( mGeometryColumn ) ), tr( "PostGIS" ) );
|
|
|
|
return false;
|
|
}
|
|
|
|
if ( result.PQntuples() > 1 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "TopoGeometry columns with data referencing different topology layers are not supported (%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();
|
|
mTopoLayerInfo.layerLevel = result.PQgetvalue( 0, 2 ).toInt();
|
|
switch ( result.PQgetvalue( 0, 3 ).toInt() )
|
|
{
|
|
case 1:
|
|
mTopoLayerInfo.featureType = TopoLayerInfo::Puntal;
|
|
break;
|
|
case 2:
|
|
mTopoLayerInfo.featureType = TopoLayerInfo::Lineal;
|
|
break;
|
|
case 3:
|
|
mTopoLayerInfo.featureType = TopoLayerInfo::Polygonal;
|
|
break;
|
|
case 4:
|
|
default:
|
|
mTopoLayerInfo.featureType = TopoLayerInfo::Mixed;
|
|
break;
|
|
}
|
|
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 ) );
|
|
|
|
QgsDebugMsgLevel( "TopoGeom orphans cleanup query: " + sql, 2 );
|
|
|
|
connectionRW()->LoggedPQexecNR( "QgsPostgresProvider", sql );
|
|
}
|
|
|
|
QString QgsPostgresProvider::geomParam( int offset ) const
|
|
{
|
|
QString geometry;
|
|
|
|
bool forceMulti = false;
|
|
|
|
if ( mSpatialColType != SctTopoGeometry )
|
|
{
|
|
forceMulti = QgsWkbTypes::isMultiType( wkbType() );
|
|
}
|
|
|
|
if ( mSpatialColType == SctTopoGeometry )
|
|
{
|
|
geometry += QLatin1String( "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;
|
|
QString delim;
|
|
int offset = 1;
|
|
|
|
QStringList defaultValues;
|
|
QList<int> 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;
|
|
bool overrideIdentity = false;
|
|
|
|
if ( ( mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktUint64 ) )
|
|
{
|
|
if ( mPrimaryKeyAttrs.size() == 1 && defaultValueClause( mPrimaryKeyAttrs[0] ).startsWith( "nextval(" ) )
|
|
{
|
|
bool foundNonEmptyPK = false;
|
|
int idx = mPrimaryKeyAttrs[0];
|
|
QString defaultValue = defaultValueClause( idx );
|
|
for ( int i = 0; i < flist.size(); i++ )
|
|
{
|
|
QgsAttributes attrs2 = flist[i].attributes();
|
|
QVariant v2 = attrs2.value( idx, QgsVariantUtils::createNullVariant( QMetaType::Type::Int ) );
|
|
// a PK field with a sequence val is auto populate by QGIS with this default
|
|
// we are only interested in non default values
|
|
if ( !QgsVariantUtils::isNull( v2 ) && v2.toString() != defaultValue && !QgsVariantUtils::isUnsetAttributeValue( v2 ) )
|
|
{
|
|
foundNonEmptyPK = true;
|
|
break;
|
|
}
|
|
}
|
|
skipSinglePKField = !foundNonEmptyPK;
|
|
}
|
|
|
|
if ( !skipSinglePKField )
|
|
{
|
|
for ( int idx : std::as_const( mPrimaryKeyAttrs ) )
|
|
{
|
|
auto identityIt = mIdentityFields.constFind( idx );
|
|
if ( identityIt != mIdentityFields.constEnd() && *identityIt == 'a' )
|
|
overrideIdentity = true;
|
|
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 )
|
|
{
|
|
const QVariant attributeValue = attributevec.value( idx, QgsVariantUtils::createNullVariant( QMetaType::Type::Int ) ); // default to NULL for missing attributes
|
|
if ( skipSinglePKField && idx == mPrimaryKeyAttrs[0] )
|
|
continue;
|
|
if ( fieldId.contains( idx ) )
|
|
continue;
|
|
|
|
if ( idx >= mAttributeFields.count() )
|
|
continue;
|
|
|
|
const QString fieldName = mAttributeFields.at( idx ).name();
|
|
if ( !mGeneratedValues.value( idx, QString() ).isEmpty() )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Skipping field %1 (idx %2) which is GENERATED." ).arg( fieldName, QString::number( idx ) ), 2 );
|
|
continue;
|
|
}
|
|
|
|
QString fieldTypeName = mAttributeFields.at( idx ).typeName();
|
|
|
|
QgsDebugMsgLevel( "Checking field against: " + fieldName, 2 );
|
|
|
|
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, QgsVariantUtils::createNullVariant( QMetaType::Type::Int ) ); // default to NULL for missing attributes
|
|
|
|
if ( v2 != attributeValue )
|
|
break;
|
|
}
|
|
|
|
insert += delim + quotedIdentifier( fieldName );
|
|
|
|
auto identityIt = mIdentityFields.constFind( idx );
|
|
if ( identityIt != mIdentityFields.constEnd() && *identityIt == 'a' )
|
|
overrideIdentity = true;
|
|
|
|
QString defVal = defaultValueClause( idx );
|
|
|
|
if ( i == flist.size() )
|
|
{
|
|
if ( qgsVariantEqual( attributeValue, defVal ) || QgsVariantUtils::isUnsetAttributeValue( attributeValue ) )
|
|
{
|
|
if ( defVal.isNull() )
|
|
{
|
|
values += delim + "NULL";
|
|
}
|
|
else
|
|
{
|
|
values += delim + defVal;
|
|
}
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "geometry" ) )
|
|
{
|
|
QString val = geomAttrToString( attributeValue, connectionRO() );
|
|
values += QStringLiteral( "%1%2(%3)" )
|
|
.arg( delim, connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( val ) );
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "geography" ) )
|
|
{
|
|
values += QStringLiteral( "%1st_geographyfromtext(%2)" )
|
|
.arg( delim, quotedValue( attributeValue.toString() ) );
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "jsonb" ) )
|
|
{
|
|
values += delim + quotedJsonValue( attributeValue ) + QStringLiteral( "::jsonb" );
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "json" ) )
|
|
{
|
|
values += delim + quotedJsonValue( attributeValue ) + QStringLiteral( "::json" );
|
|
}
|
|
else if ( fieldTypeName == QLatin1String( "bytea" ) )
|
|
{
|
|
values += delim + quotedByteaValue( attributeValue );
|
|
}
|
|
//TODO: convert arrays and hstore to native types
|
|
else
|
|
{
|
|
values += delim + quotedValue( attributeValue );
|
|
}
|
|
}
|
|
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_geographyfromtext($%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 += QStringLiteral( ") %1VALUES (%2)" ).arg( overrideIdentity ? "OVERRIDING SYSTEM VALUE " : "" ).arg( values );
|
|
|
|
if ( !( flags & QgsFeatureSink::FastInsert ) )
|
|
{
|
|
if ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktUint64 )
|
|
{
|
|
insert += QLatin1String( " RETURNING " );
|
|
|
|
QString delim;
|
|
const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
|
|
for ( int idx : constMPrimaryKeyAttrs )
|
|
{
|
|
insert += delim + quotedIdentifier( mAttributeFields.at( idx ).name() );
|
|
delim = ',';
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "prepare addfeatures: %1" ).arg( insert ), 2 );
|
|
QgsPostgresResult stmt( conn->PQprepare( QStringLiteral( "addfeatures" ), insert, fieldId.size() + offset - 1, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN ) );
|
|
|
|
if ( stmt.PQresultStatus() != PGRES_COMMAND_OK )
|
|
throw PGException( stmt );
|
|
|
|
for ( QgsFeatureList::iterator feature = flist.begin(); feature != flist.end(); ++feature )
|
|
{
|
|
QgsAttributes attrs = feature->attributes();
|
|
|
|
QStringList params;
|
|
if ( !mGeometryColumn.isNull() )
|
|
{
|
|
appendGeomParam( feature->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 ) : QgsVariantUtils::createNullVariant( QMetaType::Type::Int );
|
|
|
|
QString v;
|
|
if ( QgsVariantUtils::isNull( value ) || QgsVariantUtils::isUnsetAttributeValue( value ) )
|
|
{
|
|
QgsField fld = field( attrIdx );
|
|
v = paramValue( defaultValues[i], defaultValues[i] );
|
|
feature->setAttribute( attrIdx, convertValue( fld.type(), fld.subType(), v, fld.typeName() ) );
|
|
}
|
|
else
|
|
{
|
|
// the conversion functions expects the list as a string, so convert it
|
|
if ( value.userType() == QMetaType::Type::QStringList )
|
|
{
|
|
QStringList list_vals = value.toStringList();
|
|
// all strings need to be double quoted to allow special postgres
|
|
// array characters such as {, or whitespace in the string
|
|
// but we need to escape all double quotes and backslashes
|
|
list_vals.replaceInStrings( "\\", "\\\\" );
|
|
list_vals.replaceInStrings( "\"", "\\\"" );
|
|
v = QStringLiteral( "{\"" ) + value.toStringList().join( QLatin1String( "\",\"" ) ) + QStringLiteral( "\"}" );
|
|
}
|
|
else if ( value.userType() == QMetaType::Type::QVariantList )
|
|
{
|
|
v = "{" + value.toStringList().join( "," ) + "}";
|
|
}
|
|
else
|
|
{
|
|
v = paramValue( value.toString(), defaultValues[i] );
|
|
}
|
|
|
|
if ( v != value.toString() )
|
|
{
|
|
QgsField fld = field( attrIdx );
|
|
feature->setAttribute( attrIdx, convertValue( fld.type(), fld.subType(), v, fld.typeName() ) );
|
|
}
|
|
}
|
|
|
|
params << v;
|
|
}
|
|
|
|
QgsPostgresResult result( conn->PQexecPrepared( QStringLiteral( "addfeatures" ), params, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN ) );
|
|
|
|
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 );
|
|
feature->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 )
|
|
{
|
|
feature->setId( result.PQoidValue() );
|
|
QgsDebugMsgLevel( QStringLiteral( "new fid=%1" ).arg( feature->id() ), 4 );
|
|
}
|
|
}
|
|
|
|
if ( !( flags & QgsFeatureSink::FastInsert ) )
|
|
{
|
|
// update feature ids
|
|
if ( mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktUint64 )
|
|
{
|
|
for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); ++features )
|
|
{
|
|
QgsAttributes attrs = features->attributes();
|
|
|
|
if ( mPrimaryKeyType == PktInt )
|
|
{
|
|
features->setId( PKINT2FID( STRING_TO_FID( attrs.at( mPrimaryKeyAttrs.at( 0 ) ) ) ) );
|
|
}
|
|
else
|
|
{
|
|
QVariantList primaryKeyVals;
|
|
|
|
const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
|
|
for ( int idx : constMPrimaryKeyAttrs )
|
|
{
|
|
primaryKeyVals << attrs.at( idx );
|
|
}
|
|
|
|
features->setId( mShared->lookupFid( primaryKeyVals ) );
|
|
}
|
|
QgsDebugMsgLevel( QStringLiteral( "new fid=%1" ).arg( features->id() ), 4 );
|
|
}
|
|
}
|
|
}
|
|
|
|
conn->LoggedPQexecNR( "QgsPostgresProvider", 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->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE addfeatures" ) );
|
|
returnvalue = false;
|
|
}
|
|
|
|
conn->unlock();
|
|
return returnvalue;
|
|
}
|
|
|
|
bool QgsPostgresProvider::deleteFeatures( const QgsFeatureIds &ids )
|
|
{
|
|
if ( ids.isEmpty() )
|
|
return true;
|
|
|
|
bool returnvalue = true;
|
|
|
|
if ( mIsQuery )
|
|
{
|
|
QgsDebugError( QStringLiteral( "Cannot delete features (is a query)" ) );
|
|
return false;
|
|
}
|
|
|
|
QgsPostgresConn *conn = connectionRW();
|
|
if ( !conn )
|
|
{
|
|
return false;
|
|
}
|
|
conn->lock();
|
|
|
|
try
|
|
{
|
|
conn->begin();
|
|
|
|
QgsFeatureIds chunkIds;
|
|
const int countIds = ids.size();
|
|
int i = 0;
|
|
for ( QgsFeatureIds::const_iterator it = ids.constBegin(); it != ids.constEnd(); ++it )
|
|
{
|
|
// create chunks of fids to delete, the last chunk may be smaller
|
|
chunkIds.insert( *it );
|
|
i++;
|
|
if ( chunkIds.size() < 5000 && i < countIds )
|
|
continue;
|
|
|
|
const QString sql = QStringLiteral( "DELETE FROM %1 WHERE %2" )
|
|
.arg( mQuery, whereClause( chunkIds ) );
|
|
QgsDebugMsgLevel( "delete sql: " + sql, 2 );
|
|
|
|
//send DELETE statement and do error handling
|
|
QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
throw PGException( result );
|
|
|
|
for ( QgsFeatureIds::const_iterator chunkIt = chunkIds.constBegin(); chunkIt != chunkIds.constEnd(); ++chunkIt )
|
|
{
|
|
mShared->removeFid( *chunkIt );
|
|
}
|
|
chunkIds.clear();
|
|
}
|
|
|
|
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( -ids.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 )
|
|
{
|
|
QgsDebugError( 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 );
|
|
QgsDebugMsgLevel( "truncate sql: " + sql, 2 );
|
|
|
|
//send truncate statement and do error handling
|
|
QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", 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<QgsField> &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<QgsField>::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->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK )
|
|
throw PGException( result );
|
|
|
|
for ( QList<QgsField>::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->LoggedPQexec( "QgsPostgresProvider", 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<int> idsList = ids.values();
|
|
std::sort( idsList.begin(), idsList.end(), std::greater<int>() );
|
|
|
|
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->LoggedPQexec( "QgsPostgresProvider", 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->LoggedPQexec( "QgsPostgresProvider", 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;
|
|
int numChangedFields = 0;
|
|
for ( QgsAttributeMap::const_iterator siter = attrs.constBegin(); siter != attrs.constEnd(); ++siter )
|
|
{
|
|
try
|
|
{
|
|
const QVariant attributeValue = siter.value();
|
|
if ( QgsVariantUtils::isUnsetAttributeValue( attributeValue ) )
|
|
continue;
|
|
|
|
QgsField fld = field( siter.key() );
|
|
|
|
pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() );
|
|
|
|
if ( mGeneratedValues.contains( siter.key() ) )
|
|
{
|
|
QgsLogger::warning( tr( "Changing the value of GENERATED field %1 is not allowed." ).arg( fld.name() ) );
|
|
continue;
|
|
}
|
|
|
|
numChangedFields++;
|
|
|
|
sql += delim + QStringLiteral( "%1=" ).arg( quotedIdentifier( fld.name() ) );
|
|
delim = ',';
|
|
|
|
QString defVal = defaultValueClause( siter.key() );
|
|
if ( qgsVariantEqual( attributeValue, defVal ) )
|
|
{
|
|
sql += defVal.isNull() ? "NULL" : defVal;
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "geometry" ) )
|
|
{
|
|
QString val = geomAttrToString( attributeValue, connectionRO() );
|
|
|
|
sql += QStringLiteral( "%1(%2)" )
|
|
.arg( connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( val ) );
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "geography" ) )
|
|
{
|
|
sql += QStringLiteral( "st_geographyfromtext(%1)" )
|
|
.arg( quotedValue( attributeValue.toString() ) );
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "jsonb" ) )
|
|
{
|
|
sql += QStringLiteral( "%1::jsonb" )
|
|
.arg( quotedJsonValue( attributeValue ) );
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "json" ) )
|
|
{
|
|
sql += QStringLiteral( "%1::json" )
|
|
.arg( quotedJsonValue( attributeValue ) );
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "bytea" ) )
|
|
{
|
|
sql += quotedByteaValue( attributeValue );
|
|
}
|
|
else
|
|
{
|
|
sql += quotedValue( attributeValue );
|
|
}
|
|
}
|
|
catch ( PGFieldNotFound )
|
|
{
|
|
// Field was missing - shouldn't happen
|
|
}
|
|
}
|
|
|
|
sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) );
|
|
|
|
// Don't try to UPDATE an empty set of values (might happen if the table only has GENERATED fields,
|
|
// or if the user only changed GENERATED fields in the form/attribute table.
|
|
if ( numChangedFields > 0 )
|
|
{
|
|
QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
throw PGException( result );
|
|
}
|
|
else // let the user know that no field was actually changed
|
|
{
|
|
QgsLogger::warning( tr( "No fields were updated on the database." ) );
|
|
}
|
|
|
|
// update feature id map if key was changed
|
|
// PktInt64 also uses a fid map even if it is a stand alone field.
|
|
if ( pkChanged && ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt64 ) )
|
|
{
|
|
QVariantList k = mShared->removeFid( fid );
|
|
|
|
int keyCount = std::min( mPrimaryKeyAttrs.size(), k.size() );
|
|
|
|
for ( int i = 0; i < keyCount; 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;
|
|
|
|
const QgsGeometry convertedGeom( convertToProviderType( geom, wkbType() ) );
|
|
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 ) );
|
|
|
|
QgsDebugMsgLevel( "getting old topogeometry id: " + getid, 2 );
|
|
|
|
result = connectionRO()->PQprepare( QStringLiteral( "getid" ), getid, 1, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK )
|
|
{
|
|
QgsDebugError( 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 ) );
|
|
QgsDebugMsgLevel( "TopoGeom swap: " + replace, 2 );
|
|
result = conn->PQprepare( QStringLiteral( "replacetopogeom" ), replace, 2, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK )
|
|
{
|
|
QgsDebugError( 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 ) );
|
|
}
|
|
|
|
QgsDebugMsgLevel( "updating: " + update, 2 );
|
|
|
|
result = conn->PQprepare( QStringLiteral( "updatefeatures" ), update, 2, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
QgsDebugError( 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 );
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "iterating over the map of changed geometries..." ), 2 );
|
|
|
|
for ( QgsGeometryMap::const_iterator iter = geometry_map.constBegin();
|
|
iter != geometry_map.constEnd();
|
|
++iter )
|
|
{
|
|
QgsDebugMsgLevel( "iterating over feature id " + FID_TO_STRING( iter.key() ), 2 );
|
|
|
|
// 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, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
|
|
if ( result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
QgsDebugError( 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();
|
|
QgsDebugMsgLevel( QStringLiteral( "Old TG id is %1" ).arg( old_tg_id ), 2 );
|
|
}
|
|
|
|
QStringList params;
|
|
appendGeomParam( *iter, params );
|
|
appendPkParams( iter.key(), params );
|
|
|
|
result = conn->PQexecPrepared( QStringLiteral( "updatefeatures" ), params, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
|
|
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->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), replace );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK )
|
|
{
|
|
QgsDebugError( 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 );
|
|
QgsDebugMsgLevel( "relation swap: " + replace, 2 );
|
|
result = conn->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), replace );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK )
|
|
{
|
|
QgsDebugError( 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->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE updatefeatures" ) );
|
|
if ( mSpatialColType == SctTopoGeometry )
|
|
{
|
|
connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE getid" ) );
|
|
conn->LoggedPQexecNR( "QgsPostgresProvider", 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->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE updatefeatures" ) );
|
|
if ( mSpatialColType == SctTopoGeometry )
|
|
{
|
|
connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE getid" ) );
|
|
conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE replacetopogeom" ) );
|
|
}
|
|
returnvalue = false;
|
|
}
|
|
|
|
conn->unlock();
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "leaving." ), 4 );
|
|
|
|
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( qgis::listToSet( attr_map.keys() ) );
|
|
ids |= qgis::listToSet( geometry_map.keys() );
|
|
|
|
// cycle through the features
|
|
const auto constIds = ids;
|
|
for ( QgsFeatureId fid : constIds )
|
|
{
|
|
// 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;
|
|
int numChangedFields = 0;
|
|
|
|
for ( QgsAttributeMap::const_iterator siter = attrs.constBegin(); siter != attrs.constEnd(); ++siter )
|
|
{
|
|
try
|
|
{
|
|
QgsField fld = field( siter.key() );
|
|
|
|
pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() );
|
|
|
|
if ( mGeneratedValues.contains( siter.key() ) )
|
|
{
|
|
QgsLogger::warning( tr( "Changing the value of GENERATED field %1 is not allowed." ).arg( fld.name() ) );
|
|
continue;
|
|
}
|
|
|
|
const QVariant value = siter.value();
|
|
if ( QgsVariantUtils::isUnsetAttributeValue( value ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
numChangedFields++;
|
|
|
|
sql += delim + QStringLiteral( "%1=" ).arg( quotedIdentifier( fld.name() ) );
|
|
delim = ',';
|
|
|
|
if ( fld.typeName() == QLatin1String( "geometry" ) )
|
|
{
|
|
QString val = geomAttrToString( value, connectionRO() );
|
|
sql += QStringLiteral( "%1(%2)" )
|
|
.arg( connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( val ) );
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "geography" ) )
|
|
{
|
|
sql += QStringLiteral( "st_geographyfromtext(%1)" )
|
|
.arg( quotedValue( value.toString() ) );
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "jsonb" ) )
|
|
{
|
|
sql += QStringLiteral( "%1::jsonb" )
|
|
.arg( quotedJsonValue( value ) );
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "json" ) )
|
|
{
|
|
sql += QStringLiteral( "%1::json" )
|
|
.arg( quotedJsonValue( value ) );
|
|
}
|
|
else if ( fld.typeName() == QLatin1String( "bytea" ) )
|
|
{
|
|
sql += quotedByteaValue( value );
|
|
}
|
|
else
|
|
{
|
|
sql += quotedValue( value );
|
|
}
|
|
}
|
|
catch ( PGFieldNotFound )
|
|
{
|
|
// Field was missing - shouldn't happen
|
|
}
|
|
}
|
|
|
|
if ( !geometry_map.contains( fid ) )
|
|
{
|
|
// Don't try to UPDATE an empty set of values (might happen if the table only has GENERATED fields,
|
|
// or if the user only changed GENERATED fields in the form/attribute table.
|
|
if ( numChangedFields > 0 )
|
|
{
|
|
sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) );
|
|
|
|
QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
throw PGException( result );
|
|
}
|
|
else // let the user know that nothing has actually changed
|
|
{
|
|
QgsLogger::warning( tr( "No fields/geometries were updated on the database." ) );
|
|
}
|
|
}
|
|
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, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN ) );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
QgsDebugError( 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, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
conn->rollback();
|
|
conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeature" ) );
|
|
throw PGException( result );
|
|
}
|
|
|
|
conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE updatefeature" ) );
|
|
}
|
|
|
|
// update feature id map if key was changed
|
|
// PktInt64 also uses a fid map even though it is a single field.
|
|
if ( pkChanged && ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt64 ) )
|
|
{
|
|
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();
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "leaving." ), 4 );
|
|
|
|
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;
|
|
}
|
|
|
|
QString QgsPostgresProvider::geometryColumnName() const
|
|
{
|
|
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
|
|
|
return mGeometryColumn;
|
|
}
|
|
|
|
Qgis::VectorProviderCapabilities QgsPostgresProvider::capabilities() const
|
|
{
|
|
return mEnabledCapabilities;
|
|
}
|
|
|
|
Qgis::VectorDataProviderAttributeEditCapabilities QgsPostgresProvider::attributeEditCapabilities() const
|
|
{
|
|
return Qgis::VectorDataProviderAttributeEditCapability::EditComment;
|
|
}
|
|
|
|
Qgis::SpatialIndexPresence QgsPostgresProvider::hasSpatialIndex() const
|
|
{
|
|
QgsPostgresProviderConnection conn( mUri.uri(), QVariantMap() );
|
|
try
|
|
{
|
|
return conn.spatialIndexExists( mUri.schema(), mUri.table(), mUri.geometryColumn() ) ? Qgis::SpatialIndexPresence::Present : Qgis::SpatialIndexPresence::NotPresent;
|
|
}
|
|
catch ( QgsProviderConnectionException & )
|
|
{
|
|
return Qgis::SpatialIndexPresence::Unknown;
|
|
}
|
|
}
|
|
|
|
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()->LoggedPQexec( "QgsPostgresProvider", 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
|
|
|
|
// clone the share because the feature subset differs (and thus also the feature count etc)
|
|
mShared = mShared->clone();
|
|
|
|
// 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 )
|
|
{
|
|
reloadData();
|
|
}
|
|
else
|
|
{
|
|
mLayerExtent.reset();
|
|
emit dataChanged();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool QgsPostgresProvider::supportsSubsetString() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
QString QgsPostgresProvider::subsetStringDialect() const
|
|
{
|
|
return tr( "PostgreSQL WHERE clause" );
|
|
}
|
|
|
|
QString QgsPostgresProvider::subsetStringHelpUrl() const
|
|
{
|
|
return QStringLiteral( "https://www.postgresql.org/docs/current/sql-expressions.html" );
|
|
}
|
|
|
|
long long QgsPostgresProvider::featureCount() const
|
|
{
|
|
long long featuresCounted = mShared->featuresCounted();
|
|
if ( featuresCounted >= 0 )
|
|
return featuresCounted;
|
|
|
|
// See: https://github.com/qgis/QGIS/issues/25285 - QGIS crashes on featureCount())
|
|
if ( !connectionRO() )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// get total number of features
|
|
QString sql;
|
|
|
|
long long num = -1;
|
|
if ( !mIsQuery && mUseEstimatedMetadata )
|
|
{
|
|
if ( ( relkind() == Qgis::PostgresRelKind::View || !mSqlWhereClause.isEmpty() ) && connectionRO()->pgVersion() >= 90000 )
|
|
{
|
|
// parse explain output to estimate feature count
|
|
// we don't use pg_class reltuples because it returns 0 for view
|
|
sql = QStringLiteral( "EXPLAIN (FORMAT JSON) SELECT 1 FROM %1%2" ).arg( mQuery, filterWhereClause() );
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
const QString json = result.PQgetvalue( 0, 0 );
|
|
const QVariantList explain = QgsJsonUtils::parseJson( json ).toList();
|
|
const QVariantMap countPlan = !explain.isEmpty() ? explain[0].toMap().value( "Plan" ).toMap() : QVariantMap();
|
|
const QVariant nbRows = countPlan.value( "Plan Rows" );
|
|
|
|
if ( nbRows.isValid() )
|
|
num = nbRows.toLongLong();
|
|
else
|
|
QgsLogger::warning( QStringLiteral( "Cannot parse JSON explain result to estimate feature count (%1) : %2" ).arg( sql, json ) );
|
|
}
|
|
else
|
|
{
|
|
sql = QStringLiteral( "SELECT reltuples::bigint FROM pg_catalog.pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) );
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
num = result.PQgetvalue( 0, 0 ).toLongLong();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sql = QStringLiteral( "SELECT count(*) FROM %1%2" ).arg( mQuery, filterWhereClause() );
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
QgsDebugMsgLevel( "number of features as text: " + result.PQgetvalue( 0, 0 ), 2 );
|
|
|
|
num = result.PQgetvalue( 0, 0 ).toLongLong();
|
|
}
|
|
|
|
mShared->setFeaturesCounted( num );
|
|
|
|
QgsDebugMsgLevel( "number of features: " + QString::number( num ), 2 );
|
|
|
|
return num;
|
|
}
|
|
|
|
bool QgsPostgresProvider::empty() const
|
|
{
|
|
QString sql = QStringLiteral( "SELECT EXISTS (SELECT * FROM %1%2 LIMIT 1)" ).arg( mQuery, filterWhereClause() );
|
|
QgsPostgresResult res( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( res.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
pushError( res.PQresultErrorMessage() );
|
|
return false;
|
|
}
|
|
|
|
return res.PQgetvalue( 0, 0 ) != QLatin1String( "t" );
|
|
}
|
|
|
|
QgsRectangle QgsPostgresProvider::extent() const
|
|
{
|
|
return extent3D().toRectangle();
|
|
}
|
|
|
|
bool QgsPostgresProvider::estimateExtent() const
|
|
{
|
|
// Cannot estimate extent of a query
|
|
if ( mIsQuery )
|
|
{
|
|
QgsDebugMsgLevel( "Estimating extent of queries is not supported", 2 );
|
|
return false;
|
|
}
|
|
|
|
const int vmaj = connectionRO()->majorVersion();
|
|
const int vmin = connectionRO()->minorVersion();
|
|
|
|
if ( mSpatialColType == SctGeography )
|
|
{
|
|
// PostGIS up to PostGIS-3.4.x had bogus estimation
|
|
// for geography type, https://trac.osgeo.org/postgis/ticket/5734
|
|
if ( vmaj < 3 || ( vmaj == 3 && vmin < 5 ) )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Estimating extent of geography columns was not supported by PostGIS %1.%2 (3.5+ required)" ).arg( vmaj, vmin ), 2 );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
QString sql = QStringLiteral( "SELECT %1(%2,%3,%4)" )
|
|
.arg(
|
|
vmaj < 2 ? "estimated_extent" : ( vmaj == 2 && vmin < 1 ? "st_estimated_extent" : "st_estimatedextent" ),
|
|
quotedValue( mSchemaName ),
|
|
quotedValue( mTableName ),
|
|
quotedValue( mGeometryColumn )
|
|
);
|
|
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
if ( result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
pushError( result.PQresultErrorMessage() );
|
|
return false;
|
|
}
|
|
if ( result.PQntuples() != 1 )
|
|
{
|
|
pushError( tr( "Unexpected number of tuples from estimated extent query %1: %2 (1 expected)." )
|
|
.arg( sql )
|
|
.arg( result.PQntuples() ) );
|
|
return false;
|
|
}
|
|
|
|
if ( result.PQgetisnull( 0, 0 ) )
|
|
return false;
|
|
|
|
QString box2dString = result.PQgetvalue( 0, 0 );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Got estimated extent (%1) using: %2" ).arg( box2dString ).arg( sql ), 2 );
|
|
|
|
const thread_local QRegularExpression rx2d( "\\((.+) (.+),(.+) (.+)\\)" );
|
|
const QRegularExpressionMatch match = rx2d.match( box2dString );
|
|
if ( !match.hasMatch() )
|
|
{
|
|
pushError( tr( "Unexpected format from estimated extent query %1: %2." ).arg( sql, box2dString ) );
|
|
return false; // throw instead ?
|
|
}
|
|
|
|
mLayerExtent.emplace(
|
|
match.captured( 1 ).toDouble(), // xmin
|
|
match.captured( 2 ).toDouble(), // ymin
|
|
std::numeric_limits<double>::quiet_NaN(), // zmin
|
|
match.captured( 3 ).toDouble(), // xmax
|
|
match.captured( 4 ).toDouble(), // ymax
|
|
std::numeric_limits<double>::quiet_NaN() // zmax
|
|
);
|
|
|
|
QgsDebugMsgLevel( "Set extents to estimated value: " + mLayerExtent->toString(), 2 );
|
|
return true;
|
|
}
|
|
|
|
bool QgsPostgresProvider::computeExtent3D() const
|
|
{
|
|
QString sql = QStringLiteral( "SELECT %1(%2%3) FROM %4%5" )
|
|
.arg( connectionRO()->majorVersion() < 2 ? "extent" : "ST_3DExtent", quotedIdentifier( mBoundingBoxColumn ), ( mSpatialColType == SctPcPatch || mSpatialColType == SctGeography ) ? "::geometry" : "", mQuery, filterWhereClause() );
|
|
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
|
|
if ( result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
pushError( result.PQresultErrorMessage() );
|
|
return false;
|
|
}
|
|
|
|
if ( result.PQntuples() != 1 )
|
|
{
|
|
pushError( tr( "Unexpected number of tuples from compute extent query %1: %2 (1 expected)." )
|
|
.arg( sql )
|
|
.arg( result.PQntuples() ) );
|
|
return false;
|
|
}
|
|
|
|
if ( result.PQgetisnull( 0, 0 ) )
|
|
{
|
|
// Layer is empty, set layerExtent to null (default-construct)
|
|
QgsDebugMsgLevel( QStringLiteral( "Got null from extent aggregate, setting layer extent to null as well" ), 2 );
|
|
mLayerExtent.emplace(); // constructs a NULL
|
|
return true;
|
|
}
|
|
|
|
QString ext = result.PQgetvalue( 0, 0 );
|
|
|
|
if ( ext.isEmpty() )
|
|
{
|
|
pushError( tr( "Unexpected empty result from extent query %1." ).arg( sql ) );
|
|
return false;
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Got extents (%1) using: %2" ).arg( ext ).arg( sql ), 2 );
|
|
|
|
// Try the BOX3D format
|
|
const thread_local QRegularExpression rx3d( "\\((.+) (.+) (.+),(.+) (.+) (.+)\\)" );
|
|
QRegularExpressionMatch match = rx3d.match( ext );
|
|
if ( match.hasMatch() )
|
|
{
|
|
mLayerExtent.emplace(
|
|
match.captured( 1 ).toDouble(), // xmin
|
|
match.captured( 2 ).toDouble(), // ymin
|
|
match.captured( 3 ).toDouble(), // zmin
|
|
match.captured( 4 ).toDouble(), // xmax
|
|
match.captured( 5 ).toDouble(), // ymax
|
|
match.captured( 6 ).toDouble() // zmax
|
|
);
|
|
QgsDebugMsgLevel( "Set extents to computed 3D value: " + mLayerExtent->toString(), 2 );
|
|
if ( !elevationProperties()->containsElevationData() )
|
|
{
|
|
// TODO: add a QgsBox3D::force2D method
|
|
mLayerExtent->setZMinimum( std::numeric_limits<double>::quiet_NaN() );
|
|
mLayerExtent->setZMaximum( std::numeric_limits<double>::quiet_NaN() );
|
|
QgsDebugMsgLevel( "Removed Z from extent as layer is configured to not have elevation properties", 2 );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Try the BOX2D format
|
|
const thread_local QRegularExpression rx2d( "\\((.+) (.+),(.+) (.+)\\)" );
|
|
match = rx2d.match( ext );
|
|
if ( match.hasMatch() )
|
|
{
|
|
mLayerExtent.emplace(
|
|
match.captured( 1 ).toDouble(), // xmin
|
|
match.captured( 2 ).toDouble(), // ymin
|
|
std::numeric_limits<double>::quiet_NaN(), // zmin
|
|
match.captured( 3 ).toDouble(), // xmax
|
|
match.captured( 4 ).toDouble(), // ymax
|
|
std::numeric_limits<double>::quiet_NaN() // zmax
|
|
);
|
|
QgsDebugMsgLevel( "Set extents to computed 2D value: " + mLayerExtent->toString(), 2 );
|
|
return true;
|
|
}
|
|
|
|
QgsMessageLog::logMessage( tr( "Unexpected result from extent query %1: %2" ).arg( sql, ext ), tr( "PostGIS" ) );
|
|
return false;
|
|
}
|
|
|
|
QgsBox3D QgsPostgresProvider::extent3D() const
|
|
{
|
|
if ( !isValid() || mGeometryColumn.isNull() )
|
|
return QgsBox3D();
|
|
|
|
if ( mLayerExtent.has_value() )
|
|
return *mLayerExtent;
|
|
|
|
// Return the estimated extents, if requested and possible
|
|
if ( mUseEstimatedMetadata )
|
|
estimateExtent();
|
|
|
|
// Compute the extents, if estimation failed or was disabled
|
|
if ( !mLayerExtent.has_value() )
|
|
computeExtent3D();
|
|
|
|
if ( mLayerExtent.has_value() )
|
|
{
|
|
return *mLayerExtent;
|
|
}
|
|
else
|
|
{
|
|
pushError( tr( "Could not extract layer extent" ) );
|
|
return QgsBox3D();
|
|
}
|
|
}
|
|
|
|
void QgsPostgresProvider::updateExtents()
|
|
{
|
|
mLayerExtent.reset();
|
|
}
|
|
|
|
bool QgsPostgresProvider::getGeometryDetails()
|
|
{
|
|
if ( mGeometryColumn.isNull() )
|
|
{
|
|
mDetectedGeomType = Qgis::WkbType::NoGeometry;
|
|
mValid = true;
|
|
return true;
|
|
}
|
|
|
|
QgsPostgresResult result;
|
|
QString sql;
|
|
|
|
QString schemaName = mSchemaName;
|
|
QString tableName = mTableName;
|
|
QString geomCol = mGeometryColumn;
|
|
QString geomColType;
|
|
|
|
// Trust the datasource config means that we used requested geometry type and srid
|
|
// We only need to get the spatial column type
|
|
if ( ( mReadFlags & Qgis::DataProviderReadFlag::TrustDataSource ) && mRequestedGeomType != Qgis::WkbType::Unknown && !mRequestedSrid.isEmpty() )
|
|
{
|
|
if ( mIsQuery )
|
|
{
|
|
sql = QStringLiteral(
|
|
"SELECT t.typname FROM pg_type t inner join (SELECT pg_typeof(%1) typeof FROM %2 LIMIT 1) g ON oid = g.typeof"
|
|
)
|
|
.arg( quotedIdentifier( geomCol ), mQuery );
|
|
}
|
|
else
|
|
{
|
|
sql = QStringLiteral(
|
|
"SELECT t.typname FROM pg_type t inner join (SELECT pg_typeof(%1) typeof FROM %2.%3 LIMIT 1) g ON oid = g.typeof"
|
|
)
|
|
.arg( quotedIdentifier( geomCol ), quotedIdentifier( schemaName ), quotedIdentifier( tableName ) );
|
|
}
|
|
QgsDebugMsgLevel( QStringLiteral( "Getting the spatial column type: %1" ).arg( sql ), 2 );
|
|
|
|
result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
if ( PGRES_TUPLES_OK == result.PQresultStatus() )
|
|
{
|
|
geomColType = result.PQgetvalue( 0, 0 );
|
|
|
|
// Get spatial col type
|
|
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;
|
|
|
|
// Use requested geometry type and srid
|
|
mDetectedGeomType = mRequestedGeomType;
|
|
mDetectedSrid = mRequestedSrid;
|
|
mValid = true;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
mValid = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ( mIsQuery )
|
|
{
|
|
sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Getting geometry column: %1" ).arg( sql ), 2 );
|
|
|
|
QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( PGRES_TUPLES_OK == result.PQresultStatus() )
|
|
{
|
|
Oid tableoid = result.PQftable( 0 );
|
|
int column = result.PQftablecol( 0 );
|
|
|
|
result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", 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()->LoggedPQexec( "QgsPostgresProvider", 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()->LoggedPQexec( "QgsPostgresProvider", 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;
|
|
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 ) );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Getting geometry column: %1" ).arg( sql ), 2 );
|
|
result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
QgsDebugMsgLevel( QStringLiteral( "Geometry column query returned %1 rows" ).arg( result.PQntuples() ), 2 );
|
|
|
|
if ( result.PQntuples() == 1 )
|
|
{
|
|
detectedType = result.PQgetvalue( 0, 0 );
|
|
|
|
// Do not override the SRID if set in the data source URI
|
|
if ( detectedSrid.isEmpty() )
|
|
{
|
|
detectedSrid = result.PQgetvalue( 0, 1 );
|
|
}
|
|
|
|
QString dim = result.PQgetvalue( 0, 2 );
|
|
if ( dim == QLatin1String( "3" ) && !detectedType.endsWith( 'M' ) )
|
|
detectedType += QLatin1Char( 'Z' );
|
|
else if ( dim == QLatin1String( "4" ) )
|
|
detectedType += QLatin1String( "ZM" );
|
|
|
|
QString ds = result.PQgetvalue( 0, 1 );
|
|
if ( ds != QLatin1String( "0" ) )
|
|
detectedSrid = ds;
|
|
mSpatialColType = SctGeometry;
|
|
}
|
|
else
|
|
{
|
|
connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", 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 ) );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Getting geography column: %1" ).arg( sql ), 2 );
|
|
result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
|
|
QgsDebugMsgLevel( QStringLiteral( "Geography column query returned %1" ).arg( result.PQntuples() ), 2 );
|
|
|
|
if ( result.PQntuples() == 1 )
|
|
{
|
|
QString dt = result.PQgetvalue( 0, 0 );
|
|
if ( dt != "GEOMETRY" )
|
|
detectedType = dt;
|
|
QString ds = result.PQgetvalue( 0, 1 );
|
|
if ( ds != "0" )
|
|
detectedSrid = ds;
|
|
mSpatialColType = SctGeography;
|
|
}
|
|
else
|
|
{
|
|
connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", 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 ) );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Getting TopoGeometry column: %1" ).arg( sql ), 2 );
|
|
result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
|
|
QgsDebugMsgLevel( QStringLiteral( "TopoGeometry column query returned %1" ).arg( result.PQntuples() ), 2 );
|
|
|
|
if ( result.PQntuples() == 1 )
|
|
{
|
|
detectedType = result.PQgetvalue( 0, 0 );
|
|
detectedSrid = result.PQgetvalue( 0, 1 );
|
|
mSpatialColType = SctTopoGeometry;
|
|
}
|
|
else
|
|
{
|
|
connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", 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 ) );
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Getting pointcloud column: %1" ).arg( sql ), 2 );
|
|
result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
|
|
QgsDebugMsgLevel( QStringLiteral( "Pointcloud column query returned %1" ).arg( result.PQntuples() ), 2 );
|
|
|
|
if ( result.PQntuples() == 1 )
|
|
{
|
|
detectedType = result.PQgetvalue( 0, 0 );
|
|
detectedSrid = result.PQgetvalue( 0, 1 );
|
|
mSpatialColType = SctPcPatch;
|
|
}
|
|
else
|
|
{
|
|
connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", 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 ) );
|
|
QgsDebugMsgLevel( QStringLiteral( "Getting column datatype: %1" ).arg( sql ), 2 );
|
|
result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
|
|
QgsDebugMsgLevel( QStringLiteral( "Column datatype query returned %1" ).arg( result.PQntuples() ), 2 );
|
|
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()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery );
|
|
result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", 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()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
|
|
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 == Qgis::WkbType::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType );
|
|
detectedSrid = mRequestedSrid;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
|
|
detectedType = mRequestedGeomType == Qgis::WkbType::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mValid = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
mDetectedGeomType = QgsPostgresConn::wkbTypeFromPostgis( detectedType );
|
|
mDetectedSrid = detectedSrid;
|
|
|
|
if ( mDetectedGeomType == Qgis::WkbType::Unknown )
|
|
{
|
|
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/detected
|
|
if ( mRequestedGeomType == Qgis::WkbType::Unknown || mDetectedSrid.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++ )
|
|
{
|
|
Qgis::WkbType wkbType = layerProperty.types.at( i );
|
|
|
|
if ( ( wkbType != Qgis::WkbType::Unknown && ( mRequestedGeomType == Qgis::WkbType::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 ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Detected SRID is %1" ).arg( mDetectedSrid ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Requested SRID is %1" ).arg( mRequestedSrid ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Detected type is %1" ).arg( qgsEnumValueToKey( mDetectedGeomType ) ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Requested type is %1" ).arg( qgsEnumValueToKey( mRequestedGeomType ) ), 2 );
|
|
|
|
mValid = ( mDetectedGeomType != Qgis::WkbType::Unknown || mRequestedGeomType != Qgis::WkbType::Unknown )
|
|
&& ( !mDetectedSrid.isEmpty() || !mRequestedSrid.isEmpty() );
|
|
|
|
if ( !mValid )
|
|
return false;
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Spatial column type is %1" ).arg( QgsPostgresConn::displayStringForGeomType( mSpatialColType ) ), 2 );
|
|
|
|
return mValid;
|
|
}
|
|
|
|
bool QgsPostgresProvider::convertField( QgsField &field, const QMap<QString, QVariant> *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 QMetaType::Type::LongLong:
|
|
fieldType = QStringLiteral( "int8" );
|
|
fieldPrec = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::QDateTime:
|
|
fieldType = QStringLiteral( "timestamp without time zone" );
|
|
break;
|
|
|
|
case QMetaType::Type::QTime:
|
|
fieldType = QStringLiteral( "time" );
|
|
break;
|
|
|
|
case QMetaType::Type::QString:
|
|
fieldType = stringFieldType;
|
|
fieldPrec = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::Int:
|
|
fieldType = QStringLiteral( "int4" );
|
|
fieldPrec = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::QDate:
|
|
fieldType = QStringLiteral( "date" );
|
|
fieldPrec = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::QVariantMap:
|
|
fieldType = field.typeName();
|
|
if ( fieldType.isEmpty() )
|
|
fieldType = QStringLiteral( "hstore" );
|
|
fieldPrec = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::QStringList:
|
|
fieldType = QStringLiteral( "_text" );
|
|
fieldPrec = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::QVariantList:
|
|
{
|
|
QgsField sub( QString(), field.subType(), QString(), fieldSize, fieldPrec );
|
|
if ( !convertField( sub, nullptr ) )
|
|
return false;
|
|
fieldType = "_" + sub.typeName();
|
|
fieldPrec = 0;
|
|
break;
|
|
}
|
|
|
|
case QMetaType::Type::Double:
|
|
if ( fieldSize > 18 )
|
|
{
|
|
fieldType = QStringLiteral( "numeric" );
|
|
fieldSize = -1;
|
|
}
|
|
else
|
|
{
|
|
fieldType = QStringLiteral( "float8" );
|
|
}
|
|
fieldPrec = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::Bool:
|
|
fieldType = QStringLiteral( "bool" );
|
|
fieldPrec = 0;
|
|
fieldSize = -1;
|
|
break;
|
|
|
|
case QMetaType::Type::QByteArray:
|
|
fieldType = QStringLiteral( "bytea" );
|
|
fieldPrec = 0;
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
field.setTypeName( fieldType );
|
|
field.setLength( fieldSize );
|
|
field.setPrecision( fieldPrec );
|
|
return true;
|
|
}
|
|
|
|
|
|
void postgisGeometryType( Qgis::WkbType wkbType, QString &geometryType, int &dim )
|
|
{
|
|
dim = 2;
|
|
Qgis::WkbType flatType = QgsWkbTypes::flatType( wkbType );
|
|
geometryType = QgsWkbTypes::displayString( flatType ).toUpper();
|
|
switch ( flatType )
|
|
{
|
|
case Qgis::WkbType::Unknown:
|
|
geometryType = QStringLiteral( "GEOMETRY" );
|
|
break;
|
|
|
|
case Qgis::WkbType::NoGeometry:
|
|
geometryType.clear();
|
|
dim = 0;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if ( QgsWkbTypes::hasZ( wkbType ) && QgsWkbTypes::hasM( wkbType ) )
|
|
{
|
|
dim = 4;
|
|
}
|
|
else if ( QgsWkbTypes::hasZ( wkbType ) )
|
|
{
|
|
dim = 3;
|
|
}
|
|
else if ( QgsWkbTypes::hasM( wkbType ) )
|
|
{
|
|
geometryType += QLatin1Char( 'M' );
|
|
dim = 3;
|
|
}
|
|
else if ( wkbType >= Qgis::WkbType::Point25D && wkbType <= Qgis::WkbType::MultiPolygon25D )
|
|
{
|
|
dim = 3;
|
|
}
|
|
}
|
|
|
|
Qgis::VectorExportResult QgsPostgresProvider::createEmptyLayer( const QString &uri, const QgsFields &fields, Qgis::WkbType wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap<int, int> *oldToNewAttrIdxMap, QString &createdLayerUri, QString *errorMessage, const QMap<QString, QVariant> *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 );
|
|
createdLayerUri = uri;
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Connection info is: %1" ).arg( QgsPostgresConn::connectionInfo( dsUri, false ) ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Geometry column is: %1" ).arg( geometryColumn ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Schema is: %1" ).arg( schemaName ), 2 );
|
|
QgsDebugMsgLevel( QStringLiteral( "Table name is: %1" ).arg( tableName ), 2 );
|
|
|
|
// create the table
|
|
QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, false );
|
|
if ( !conn )
|
|
{
|
|
if ( errorMessage )
|
|
*errorMessage = QObject::tr( "Connection to database failed" );
|
|
return Qgis::VectorExportResult::ErrorConnectionFailed;
|
|
}
|
|
|
|
// get the pk's name and type
|
|
// Try to find a PK candidate from numeric NOT NULL / UNIQUE columns
|
|
if ( primaryKey.isEmpty() )
|
|
{
|
|
for ( const auto &field : std::as_const( fields ) )
|
|
{
|
|
if ( field.isNumeric() && ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintUnique ) && ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull ) && ( field.constraints().constraints() & QgsFieldConstraints::ConstraintOrigin::ConstraintOriginProvider ) )
|
|
{
|
|
primaryKey = field.name();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if no pk name was passed or guessed, 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 );
|
|
const auto constPkList = pkList;
|
|
const bool lowercaseFieldNames = options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool();
|
|
for ( const QString &col : constPkList )
|
|
{
|
|
// search for the passed field
|
|
QString type;
|
|
for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx )
|
|
{
|
|
const QString fieldName = lowercaseFieldNames ? fields[fldIdx].name().toLower() : fields[fldIdx].name();
|
|
if ( fieldName == 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->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "BEGIN" ) );
|
|
|
|
// We want a valid schema name ...
|
|
if ( schemaName.isEmpty() )
|
|
{
|
|
QString sql = QString( "SELECT current_schema" );
|
|
QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", 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->LoggedPQexec( "QgsPostgresProvider", 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->LoggedPQexec( "QgsPostgresProvider", 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 += QLatin1Char( ',' );
|
|
sql += QLatin1Char( ',' );
|
|
}
|
|
|
|
pk += col;
|
|
sql += col + " " + type;
|
|
}
|
|
sql += QStringLiteral( ", PRIMARY KEY (%1) )" ).arg( pk );
|
|
|
|
result = conn->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK )
|
|
throw PGException( result );
|
|
|
|
// get geometry type, dim and srid
|
|
int dim = 2;
|
|
long srid = srs.postgisSrid();
|
|
|
|
postgisGeometryType( 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->LoggedPQexec( "QgsPostgresProvider", sql );
|
|
if ( result.PQresultStatus() != PGRES_TUPLES_OK )
|
|
throw PGException( result );
|
|
}
|
|
else
|
|
{
|
|
geometryColumn.clear();
|
|
}
|
|
|
|
conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
|
|
}
|
|
catch ( PGException &e )
|
|
{
|
|
if ( errorMessage )
|
|
*errorMessage = QObject::tr( "Creation of data source %1 failed: \n%2" )
|
|
.arg( schemaTableName, e.errorMessage() );
|
|
|
|
conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "ROLLBACK" ) );
|
|
conn->unref();
|
|
return Qgis::VectorExportResult::ErrorCreatingLayer;
|
|
}
|
|
conn->unref();
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "layer %1 created" ).arg( schemaTableName ), 2 );
|
|
|
|
// use the provider to edit the table
|
|
dsUri.setDataSource( schemaName, tableName, geometryColumn, QString(), primaryKey );
|
|
|
|
QgsDataProvider::ProviderOptions providerOptions;
|
|
Qgis::DataProviderReadFlags flags;
|
|
auto provider = std::make_unique<QgsPostgresProvider>( dsUri.uri( false ), providerOptions, flags );
|
|
if ( !provider->isValid() )
|
|
{
|
|
if ( errorMessage )
|
|
*errorMessage = QObject::tr( "Loading of the layer %1 failed" ).arg( schemaTableName );
|
|
|
|
return Qgis::VectorExportResult::ErrorInvalidLayer;
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "layer loaded" ), 2 );
|
|
|
|
// add fields to the layer
|
|
if ( oldToNewAttrIdxMap )
|
|
oldToNewAttrIdxMap->clear();
|
|
|
|
if ( fields.size() > 0 )
|
|
{
|
|
int offset = 1;
|
|
|
|
// get the list of fields
|
|
QList<QgsField> 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
|
|
QgsDebugMsgLevel( QStringLiteral( "Found a field with the same name of the geometry column. Skip it!" ), 2 );
|
|
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 ( !( options && options->value( QStringLiteral( "skipConvertFields" ), false ).toBool() ) && !convertField( fld, options ) )
|
|
{
|
|
if ( errorMessage )
|
|
*errorMessage = QObject::tr( "Unsupported type for field %1" ).arg( fld.name() );
|
|
|
|
return Qgis::VectorExportResult::ErrorAttributeTypeUnsupported;
|
|
}
|
|
|
|
QgsDebugMsgLevel( 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() ), 2 );
|
|
|
|
flist.append( fld );
|
|
if ( oldToNewAttrIdxMap )
|
|
oldToNewAttrIdxMap->insert( fldIdx, offset++ );
|
|
}
|
|
|
|
if ( !provider->addAttributes( flist ) )
|
|
{
|
|
if ( errorMessage )
|
|
*errorMessage = QObject::tr( "Creation of fields failed:\n%1" ).arg( provider->errors().join( '\n' ) );
|
|
|
|
return Qgis::VectorExportResult::ErrorAttributeCreationFailed;
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Done creating fields" ), 2 );
|
|
}
|
|
return Qgis::VectorExportResult::Success;
|
|
}
|
|
|
|
QgsCoordinateReferenceSystem QgsPostgresProvider::crs() const
|
|
{
|
|
QgsCoordinateReferenceSystem srs;
|
|
int srid = mRequestedSrid.isEmpty() ? mDetectedSrid.toInt() : mRequestedSrid.toInt();
|
|
|
|
return sridToCrs( srid, connectionRO() );
|
|
}
|
|
|
|
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 ( auto *lConnectionRO = connectionRO() )
|
|
{
|
|
QgsPostgresResult result;
|
|
|
|
/* TODO: expose a cached QgsPostgresConn::version() ? */
|
|
result = lConnectionRO->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), QStringLiteral( "SELECT version()" ) );
|
|
if ( result.PQresultStatus() == PGRES_TUPLES_OK )
|
|
{
|
|
pgVersion = result.PQgetvalue( 0, 0 );
|
|
}
|
|
|
|
postgisVersion = lConnectionRO->postgisVersion();
|
|
}
|
|
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;
|
|
}
|
|
|
|
QString QgsPostgresProvider::getNextString( const QString &txt, int &i, const QString &sep )
|
|
{
|
|
jumpSpace( txt, i );
|
|
if ( i < txt.length() && txt.at( i ) == '"' )
|
|
{
|
|
const thread_local QRegularExpression stringRe( QRegularExpression::anchoredPattern( "^\"((?:\\\\.|[^\"\\\\])*)\".*" ) );
|
|
const QRegularExpressionMatch match = stringRe.match( txt.mid( i ) );
|
|
if ( !match.hasMatch() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Cannot find end of double quoted string: %1" ).arg( txt ), tr( "PostGIS" ) );
|
|
return QString();
|
|
}
|
|
i += match.captured( 1 ).length() + 2;
|
|
jumpSpace( txt, i );
|
|
if ( !QStringView { txt }.mid( i ).startsWith( sep ) && i < txt.length() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Cannot find separator: %1" ).arg( txt.mid( i ) ), tr( "PostGIS" ) );
|
|
return QString();
|
|
}
|
|
i += sep.length();
|
|
return match.captured( 1 ).replace( QLatin1String( "\\\"" ), QLatin1String( "\"" ) ).replace( QLatin1String( "\\\\" ), QLatin1String( "\\" ) );
|
|
}
|
|
else
|
|
{
|
|
int start = i;
|
|
for ( ; i < txt.length(); i++ )
|
|
{
|
|
if ( QStringView { txt }.mid( i ).startsWith( sep ) )
|
|
{
|
|
QStringView v( QStringView { txt }.mid( start, i - start ) );
|
|
i += sep.length();
|
|
return v.trimmed().toString();
|
|
}
|
|
}
|
|
return QStringView { txt }.mid( start, i - start ).trimmed().toString();
|
|
}
|
|
}
|
|
|
|
QVariant QgsPostgresProvider::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() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Error parsing hstore: %1" ).arg( txt ), tr( "PostGIS" ) );
|
|
break;
|
|
}
|
|
result.insert( key, value );
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QVariant QgsPostgresProvider::parseJson( const QString &txt )
|
|
{
|
|
return QgsJsonUtils::parseJson( txt );
|
|
}
|
|
|
|
QVariant QgsPostgresProvider::parseOtherArray( const QString &txt, QMetaType::Type subType, const QString &typeName, QgsPostgresConn *conn )
|
|
{
|
|
int i = 0;
|
|
QVariantList result;
|
|
while ( i < txt.length() )
|
|
{
|
|
const QString value = getNextString( txt, i, QStringLiteral( "," ) );
|
|
if ( value.isNull() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Error parsing array: %1" ).arg( txt ), tr( "PostGIS" ) );
|
|
break;
|
|
}
|
|
result.append( convertValue( subType, QMetaType::Type::UnknownType, value, typeName, conn ) );
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QVariant QgsPostgresProvider::parseStringArray( const QString &txt )
|
|
{
|
|
int i = 0;
|
|
QStringList result;
|
|
while ( i < txt.length() )
|
|
{
|
|
const QString value = getNextString( txt, i, QStringLiteral( "," ) );
|
|
if ( value.isNull() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Error parsing array: %1" ).arg( txt ), tr( "PostGIS" ) );
|
|
break;
|
|
}
|
|
result.append( value );
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QVariant QgsPostgresProvider::parseMultidimensionalArray( const QString &txt )
|
|
{
|
|
QStringList result;
|
|
if ( !txt.startsWith( '{' ) || !txt.endsWith( '}' ) )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Error parsing array, missing curly braces: %1" ).arg( txt ), tr( "PostGIS" ) );
|
|
return result;
|
|
}
|
|
|
|
QStringList values;
|
|
QString text = txt;
|
|
while ( !text.isEmpty() )
|
|
{
|
|
bool escaped = false;
|
|
int openedBrackets = 1;
|
|
int i = 0;
|
|
while ( i < text.length() && openedBrackets > 0 )
|
|
{
|
|
++i;
|
|
|
|
if ( text.at( i ) == '}' && !escaped )
|
|
openedBrackets--;
|
|
else if ( text.at( i ) == '{' && !escaped )
|
|
openedBrackets++;
|
|
|
|
escaped = !escaped ? text.at( i ) == '\\' : false;
|
|
}
|
|
|
|
values.append( text.left( ++i ) );
|
|
i = text.indexOf( ',', i );
|
|
i = i > 0 ? text.indexOf( '{', i ) : -1;
|
|
if ( i == -1 )
|
|
break;
|
|
|
|
text = text.mid( i );
|
|
}
|
|
return values;
|
|
}
|
|
|
|
QVariant QgsPostgresProvider::parseArray( const QString &txt, QMetaType::Type type, QMetaType::Type subType, const QString &typeName, QgsPostgresConn *conn )
|
|
{
|
|
if ( !txt.startsWith( '{' ) || !txt.endsWith( '}' ) )
|
|
{
|
|
if ( !txt.isEmpty() )
|
|
QgsMessageLog::logMessage( tr( "Error parsing array, missing curly braces: %1" ).arg( txt ), tr( "PostGIS" ) );
|
|
return QgsVariantUtils::createNullVariant( type );
|
|
}
|
|
QString inner = txt.mid( 1, txt.length() - 2 );
|
|
if ( ( type == QMetaType::Type::QStringList || type == QMetaType::Type::QVariantList ) && inner.startsWith( "{" ) )
|
|
return parseMultidimensionalArray( inner );
|
|
else if ( type == QMetaType::Type::QStringList )
|
|
return parseStringArray( inner );
|
|
else
|
|
return parseOtherArray( inner, subType, typeName, conn );
|
|
}
|
|
|
|
QVariant QgsPostgresProvider::convertValue( QMetaType::Type type, QMetaType::Type subType, const QString &value, const QString &typeName ) const
|
|
{
|
|
return convertValue( type, subType, value, typeName, connectionRO() );
|
|
}
|
|
|
|
QVariant QgsPostgresProvider::convertValue( QMetaType::Type type, QMetaType::Type subType, const QString &value, const QString &typeName, QgsPostgresConn *conn )
|
|
{
|
|
QVariant result;
|
|
switch ( type )
|
|
{
|
|
case QMetaType::Type::QVariantMap:
|
|
if ( typeName == QLatin1String( "json" ) || typeName == QLatin1String( "jsonb" ) )
|
|
result = parseJson( value );
|
|
else
|
|
result = parseHstore( value );
|
|
break;
|
|
case QMetaType::Type::QStringList:
|
|
case QMetaType::Type::QVariantList:
|
|
result = parseArray( value, type, subType, typeName, conn );
|
|
break;
|
|
case QMetaType::Type::Bool:
|
|
if ( value == QChar( 't' ) )
|
|
result = true;
|
|
else if ( value == QChar( 'f' ) )
|
|
result = false;
|
|
else
|
|
result = QgsVariantUtils::createNullVariant( type );
|
|
break;
|
|
case QMetaType::Type::User:
|
|
result = fromEwkt( value, conn );
|
|
break;
|
|
|
|
default:
|
|
result = value;
|
|
if ( !result.convert( type ) || value.isNull() )
|
|
result = QgsVariantUtils::createNullVariant( type );
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
QList<QgsVectorLayer *> QgsPostgresProvider::searchLayers( const QList<QgsVectorLayer *> &layers, const QString &connectionInfo, const QString &schema, const QString &tableName )
|
|
{
|
|
QList<QgsVectorLayer *> result;
|
|
const auto constLayers = layers;
|
|
for ( QgsVectorLayer *layer : constLayers )
|
|
{
|
|
const QgsPostgresProvider *pgProvider = qobject_cast<QgsPostgresProvider *>( layer->dataProvider() );
|
|
if ( pgProvider && QgsPostgresConn::connectionInfo( pgProvider->mUri, false ) == connectionInfo && pgProvider->mSchemaName == schema && pgProvider->mTableName == tableName )
|
|
{
|
|
result.append( layer );
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QList<QgsRelation> QgsPostgresProvider::discoverRelations( const QgsVectorLayer *target, const QList<QgsVectorLayer *> &layers ) const
|
|
{
|
|
QList<QgsRelation> result;
|
|
|
|
// Silently skip if this is a query layer or for some obscure reason there are no table and schema name
|
|
if ( mIsQuery || mTableName.isEmpty() || mSchemaName.isEmpty() )
|
|
{
|
|
return result;
|
|
}
|
|
|
|
// Skip less silently if layer is not valid
|
|
if ( !mValid )
|
|
{
|
|
QgsLogger::warning( tr( "Error discovering relations of %1: invalid layer" ).arg( mQuery ) );
|
|
return result;
|
|
}
|
|
|
|
|
|
QString sql(
|
|
"WITH foreign_keys AS "
|
|
" ( SELECT c.conname, "
|
|
" c.conrelid, "
|
|
" c.confrelid, "
|
|
" unnest(c.conkey) AS conkey, "
|
|
" unnest(c.confkey) AS confkey, "
|
|
" (SELECT relname "
|
|
" FROM pg_catalog.pg_class "
|
|
" WHERE oid = c.conrelid) as referencing_table, "
|
|
" (SELECT relname "
|
|
" FROM pg_catalog.pg_class "
|
|
" WHERE oid = c.confrelid) as referenced_table, "
|
|
" (SELECT relnamespace::regnamespace::text "
|
|
" FROM pg_catalog.pg_class "
|
|
" WHERE oid = c.confrelid) as constraint_schema "
|
|
" FROM pg_constraint c "
|
|
" WHERE contype = 'f' "
|
|
" AND c.conrelid::regclass = "
|
|
+ QgsPostgresConn::quotedValue( QString( QgsPostgresConn::quotedIdentifier( mSchemaName ) + '.' + QgsPostgresConn::quotedIdentifier( mTableName ) ) ) + "::regclass ) "
|
|
"SELECT fk.conname as constraint_name, "
|
|
" a.attname as column_name, "
|
|
" fk.constraint_schema, "
|
|
" referenced_table as table_name, "
|
|
" af.attname as column_name "
|
|
"FROM foreign_keys fk "
|
|
"JOIN pg_attribute af ON af.attnum = fk.confkey "
|
|
"AND af.attrelid = fk.confrelid "
|
|
"JOIN pg_attribute a ON a.attnum = conkey "
|
|
"AND a.attrelid = fk.conrelid "
|
|
"ORDER BY fk.confrelid, "
|
|
" fk.conname ;"
|
|
);
|
|
|
|
QgsPostgresResult sqlResult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
if ( sqlResult.PQresultStatus() != PGRES_TUPLES_OK )
|
|
{
|
|
QgsLogger::warning( "Error getting the foreign keys of " + mTableName );
|
|
return result;
|
|
}
|
|
|
|
QList<QString> refTableFound;
|
|
for ( int row = 0; row < sqlResult.PQntuples(); ++row )
|
|
{
|
|
const QString name = sqlResult.PQgetvalue( row, 0 );
|
|
const QString fkColumn = sqlResult.PQgetvalue( row, 1 );
|
|
QString refSchema = sqlResult.PQgetvalue( row, 2 );
|
|
QString refTable = sqlResult.PQgetvalue( row, 3 );
|
|
// Strip quotes
|
|
if ( refTable.startsWith( '"' ) && refTable.endsWith( '"' ) )
|
|
{
|
|
refTable = refTable.mid( 1, refTable.length() - 2 );
|
|
}
|
|
if ( refSchema.startsWith( '"' ) && refSchema.endsWith( '"' ) )
|
|
{
|
|
refSchema = refSchema.mid( 1, refSchema.length() - 2 );
|
|
}
|
|
const QString refColumn = sqlResult.PQgetvalue( row, 4 );
|
|
// try to find if we have layers for the referenced table
|
|
const QList<QgsVectorLayer *> foundLayers = searchLayers( layers, QgsPostgresConn::connectionInfo( mUri, false ), refSchema, refTable );
|
|
if ( !refTableFound.contains( refTable ) )
|
|
{
|
|
for ( const QgsVectorLayer *foundLayer : foundLayers )
|
|
{
|
|
QgsRelation relation;
|
|
relation.setName( name );
|
|
relation.setReferencingLayer( target->id() );
|
|
relation.setReferencedLayer( foundLayer->id() );
|
|
relation.addFieldPair( fkColumn, refColumn );
|
|
relation.generateId();
|
|
if ( relation.isValid() )
|
|
{
|
|
result.append( relation );
|
|
refTableFound.append( refTable );
|
|
}
|
|
else
|
|
{
|
|
QgsLogger::warning( "Invalid relation for " + name );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// multi reference field => add the field pair to all the referenced layers found
|
|
const int resultSize = result.size();
|
|
for ( int i = 0; i < resultSize; ++i )
|
|
{
|
|
for ( const QgsVectorLayer *foundLayer : foundLayers )
|
|
{
|
|
if ( result[resultSize - 1 - i].referencedLayerId() == foundLayer->id() )
|
|
{
|
|
result[resultSize - 1 - i].addFieldPair( fkColumn, refColumn );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void QgsPostgresProvider::setQuery( const QString &query )
|
|
{
|
|
mQuery = query;
|
|
|
|
mKind = Qgis::PostgresRelKind::NotSet;
|
|
}
|
|
|
|
Qgis::PostgresRelKind QgsPostgresProvider::relkind() const
|
|
{
|
|
if ( mKind != Qgis::PostgresRelKind::NotSet )
|
|
return mKind;
|
|
|
|
if ( mIsQuery || !connectionRO() )
|
|
{
|
|
mKind = Qgis::PostgresRelKind::Unknown;
|
|
}
|
|
else
|
|
{
|
|
QString sql = QStringLiteral( "SELECT relkind FROM pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) );
|
|
QgsPostgresResult res( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
|
|
QString type = res.PQgetvalue( 0, 0 );
|
|
|
|
mKind = QgsPostgresConn::relKindFromValue( type );
|
|
}
|
|
|
|
return mKind;
|
|
}
|
|
|
|
bool QgsPostgresProvider::hasMetadata() const
|
|
{
|
|
bool hasMetadata = true;
|
|
Qgis::PostgresRelKind kind = relkind();
|
|
|
|
if ( kind == Qgis::PostgresRelKind::View || kind == Qgis::PostgresRelKind::MaterializedView )
|
|
{
|
|
hasMetadata = false;
|
|
}
|
|
|
|
return hasMetadata;
|
|
}
|
|
|
|
QString QgsPostgresProvider::htmlMetadata() const
|
|
{
|
|
// construct multiple temporary tables to be used with PostgreSQL WITH statement
|
|
// that build one on another and are then used to create single SQL query to get the additional metadata
|
|
|
|
const QString sqlWithTableInfo = QStringLiteral( "table_info AS ("
|
|
"SELECT c.oid AS oid, n.nspname AS schema_name, c.relname AS table_name, c.reltuples::bigint AS estimate "
|
|
"FROM pg_class c "
|
|
"JOIN pg_namespace n ON c.relnamespace = n.oid "
|
|
"WHERE c.relname = %1 AND n.nspname = %2"
|
|
")" )
|
|
.arg( QgsPostgresConn::quotedValue( mTableName ), QgsPostgresConn::quotedValue( mSchemaName ) );
|
|
|
|
const QString sqlWithPrivilages = QStringLiteral( "privileges AS ("
|
|
"SELECT table_info.oid as oid, "
|
|
"COALESCE(NULLIF(CONCAT_WS(', ',"
|
|
"CASE WHEN has_table_privilege(table_info.oid, 'SELECT') THEN 'SELECT' END,"
|
|
"CASE WHEN has_table_privilege(table_info.oid, 'INSERT') THEN 'INSERT' END,"
|
|
"CASE WHEN has_table_privilege(table_info.oid, 'UPDATE') THEN 'UPDATE' END,"
|
|
"CASE WHEN has_table_privilege(table_info.oid, 'DELETE') THEN 'DELETE' END),"
|
|
"''), '') as privileges "
|
|
"FROM table_info"
|
|
")" );
|
|
|
|
const QString sqlWithIndexes = QStringLiteral( "table_indexes AS("
|
|
"SELECT pi.schemaname AS schema_name, pi.tablename AS table_name, pi.indexname AS index_name "
|
|
"FROM pg_indexes pi "
|
|
"JOIN table_info ti ON pi.schemaname = ti.schema_name AND pi.tablename = ti.table_name "
|
|
"WHERE pi.indexdef LIKE '%USING gist%' "
|
|
"),"
|
|
"table_indexes_info AS("
|
|
"SELECT schema_name, table_name, COALESCE(string_agg(index_name, ', ' ORDER BY index_name), '') as index_names "
|
|
"FROM table_indexes "
|
|
"GROUP BY schema_name, table_name"
|
|
")" );
|
|
|
|
const QString sqlMainQuery = QStringLiteral( "WITH %1, %2, %3"
|
|
"SELECT table_info.oid, table_info.table_name, table_info.schema_name, privileges.privileges, "
|
|
"table_info.estimate, table_indexes_info.index_names, pg_description.description "
|
|
"FROM table_info "
|
|
"LEFT JOIN privileges ON table_info.oid = privileges.oid "
|
|
"LEFT JOIN table_indexes_info ON table_indexes_info.table_name = table_info.table_name AND table_indexes_info.schema_name = table_info.schema_name "
|
|
"LEFT JOIN pg_description ON pg_description.objoid = table_info.oid" )
|
|
.arg( sqlWithTableInfo, sqlWithPrivilages, sqlWithIndexes );
|
|
|
|
QgsPostgresResult resTable( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sqlMainQuery ) );
|
|
|
|
QString tableComment;
|
|
QString privileges;
|
|
QString spatialIndexText = tr( "No spatial index." );
|
|
long long estimateRowCount = -1;
|
|
|
|
if ( resTable.PQntuples() > 0 )
|
|
{
|
|
tableComment = resTable.PQgetvalue( 0, 6 );
|
|
tableComment = tableComment.replace( QStringLiteral( "\n" ), QStringLiteral( "<br>" ) );
|
|
|
|
estimateRowCount = resTable.PQgetvalue( 0, 4 ).toLongLong();
|
|
|
|
privileges = resTable.PQgetvalue( 0, 3 );
|
|
|
|
if ( !resTable.PQgetvalue( 0, 5 ).isEmpty() )
|
|
{
|
|
spatialIndexText = tr( "Spatial index/indices exists (%1)." ).arg( resTable.PQgetvalue( 0, 5 ) );
|
|
}
|
|
}
|
|
|
|
const QVariantMap additionalInformation {
|
|
{ tr( "Privileges" ), privileges },
|
|
{ tr( "Rows (estimation)" ), estimateRowCount },
|
|
{ tr( "Spatial Index" ), spatialIndexText },
|
|
{ tr( "Table Comment" ), tableComment }
|
|
};
|
|
|
|
return QgsPostgresUtils::variantMapToHtml( additionalInformation, tr( "Additional information" ) );
|
|
}
|
|
|
|
QgsDataProvider *QgsPostgresProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, Qgis::DataProviderReadFlags flags )
|
|
{
|
|
return new QgsPostgresProvider( uri, options, flags );
|
|
}
|
|
|
|
QList<QgsDataItemProvider *> QgsPostgresProviderMetadata::dataItemProviders() const
|
|
{
|
|
QList<QgsDataItemProvider *> providers;
|
|
providers << new QgsPostgresDataItemProvider;
|
|
return providers;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Qgis::VectorExportResult QgsPostgresProviderMetadata::createEmptyLayer( const QString &uri, const QgsFields &fields, Qgis::WkbType wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap<int, int> &oldToNewAttrIdxMap, QString &errorMessage, const QMap<QString, QVariant> *options, QString &createdLayerUri )
|
|
{
|
|
return QgsPostgresProvider::createEmptyLayer(
|
|
uri, fields, wkbType, srs, overwrite,
|
|
&oldToNewAttrIdxMap, createdLayerUri, &errorMessage, options
|
|
);
|
|
}
|
|
|
|
bool QgsPostgresProviderMetadata::styleExists( const QString &uri, const QString &styleId, QString &errorCause )
|
|
{
|
|
errorCause.clear();
|
|
|
|
QgsDataSourceUri dsUri( uri );
|
|
QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, true );
|
|
if ( !conn )
|
|
{
|
|
errorCause = QObject::tr( "Connection to database failed" );
|
|
return false;
|
|
}
|
|
|
|
if ( !QgsPostgresUtils::tableExists( conn, QStringLiteral( "layer_styles" ) ) )
|
|
{
|
|
return false;
|
|
}
|
|
else if ( !QgsPostgresUtils::columnExists( conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( dsUri.database().isEmpty() ) // typically when a service file is used
|
|
{
|
|
dsUri.setDatabase( conn->currentDatabase() );
|
|
}
|
|
|
|
const QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
|
|
|
|
const 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 (type=%5 OR type IS NULL)"
|
|
" AND styleName=%6" )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
|
|
.arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "= %1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
|
|
.arg( wkbTypeString )
|
|
.arg( QgsPostgresConn::quotedValue( styleId.isEmpty() ? dsUri.table() : styleId ) );
|
|
|
|
QgsPostgresResult res( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), checkQuery ) );
|
|
if ( res.PQresultStatus() == PGRES_TUPLES_OK )
|
|
{
|
|
return res.PQntuples() > 0;
|
|
}
|
|
else
|
|
{
|
|
errorCause = res.PQresultErrorMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString &qmlStyleIn, const QString &sldStyleIn, const QString &styleName, const QString &styleDescription, const QString &uiFileContent, bool useAsDefault, QString &errCause )
|
|
{
|
|
QgsDataSourceUri dsUri( uri );
|
|
|
|
// Replace invalid XML characters
|
|
QString qmlStyle { qmlStyleIn };
|
|
QgsPostgresUtils::replaceInvalidXmlChars( qmlStyle );
|
|
QString sldStyle { sldStyleIn };
|
|
QgsPostgresUtils::replaceInvalidXmlChars( sldStyle );
|
|
|
|
QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, false );
|
|
if ( !conn )
|
|
{
|
|
errCause = QObject::tr( "Connection to database failed" );
|
|
return false;
|
|
}
|
|
|
|
if ( !QgsPostgresUtils::tableExists( conn, QStringLiteral( "layer_styles" ) ) )
|
|
{
|
|
if ( !QgsPostgresUtils::createStylesTable( conn, QStringLiteral( "QgsPostgresProviderMetadata" ) ) )
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( !QgsPostgresUtils::columnExists( conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) )
|
|
{
|
|
QgsPostgresResult res( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), "ALTER TABLE layer_styles ADD COLUMN type varchar NULL" ) );
|
|
if ( res.PQresultStatus() != PGRES_COMMAND_OK )
|
|
{
|
|
errCause = QObject::tr( "Unable to add column type to layer_styles table. 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 ) );
|
|
}
|
|
|
|
const QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
|
|
|
|
// 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,type%12"
|
|
") VALUES ("
|
|
"%1,%2,%3,%4,%5,XMLPARSE(DOCUMENT %16),XMLPARSE(DOCUMENT %17),%8,%9,%10,%11%13"
|
|
")" )
|
|
.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( "CURRENT_USER" )
|
|
.arg( uiFileColumn )
|
|
.arg( uiFileValue )
|
|
.arg( wkbTypeString )
|
|
// 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 (type=%5 OR type IS NULL)"
|
|
" AND styleName=%6" )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
|
|
.arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
|
|
.arg( wkbTypeString )
|
|
.arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
|
|
|
|
QgsPostgresResult res( conn->LoggedPQexec( "QgsPostgresProviderMetadata", checkQuery ) );
|
|
if ( res.PQntuples() > 0 )
|
|
{
|
|
sql = QString( "UPDATE layer_styles"
|
|
" SET useAsDefault=%1"
|
|
",styleQML=XMLPARSE(DOCUMENT %12)"
|
|
",styleSLD=XMLPARSE(DOCUMENT %13)"
|
|
",description=%4"
|
|
",owner=%5"
|
|
",type=%2"
|
|
" WHERE f_table_catalog=%6"
|
|
" AND f_table_schema=%7"
|
|
" AND f_table_name=%8"
|
|
" AND f_geometry_column %9"
|
|
" AND styleName=%10"
|
|
" AND (type=%2 OR type IS NULL)" )
|
|
.arg( useAsDefault ? "true" : "false" )
|
|
.arg( wkbTypeString )
|
|
.arg( QgsPostgresConn::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
|
|
.arg( "CURRENT_USER" )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
|
|
.arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).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"
|
|
" AND (type=%5 OR type IS NULL)" )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
|
|
.arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
|
|
.arg( wkbTypeString );
|
|
|
|
sql = QStringLiteral( "BEGIN; %1; %2; COMMIT;" ).arg( removeDefaultSql, sql );
|
|
}
|
|
|
|
res = conn->LoggedPQexec( "QgsPostgresProviderMetadata", 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;
|
|
}
|
|
|
|
|
|
QString QgsPostgresProviderMetadata::loadStyle( const QString &uri, QString &errCause )
|
|
{
|
|
QString styleName;
|
|
return loadStoredStyle( uri, styleName, errCause );
|
|
}
|
|
|
|
QString QgsPostgresProviderMetadata::loadStoredStyle( const QString &uri, QString &styleName, QString &errCause )
|
|
{
|
|
QgsDataSourceUri dsUri( uri );
|
|
QString selectQmlQuery;
|
|
|
|
QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, true );
|
|
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 ( !QgsPostgresUtils::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 wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
|
|
|
|
// support layer_styles without type column < 3.14
|
|
if ( !QgsPostgresUtils::columnExists( conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) )
|
|
{
|
|
selectQmlQuery = QString( "SELECT styleName, 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 );
|
|
}
|
|
else
|
|
{
|
|
selectQmlQuery = QString( "SELECT styleName, styleQML"
|
|
" FROM layer_styles"
|
|
" WHERE f_table_catalog=%1"
|
|
" AND f_table_schema=%2"
|
|
" AND f_table_name=%3"
|
|
" AND f_geometry_column %4"
|
|
" AND (type=%5 OR type IS NULL)"
|
|
" 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 )
|
|
.arg( wkbTypeString );
|
|
}
|
|
|
|
QgsPostgresResult result( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), selectQmlQuery ) );
|
|
|
|
styleName = result.PQntuples() == 1 ? result.PQgetvalue( 0, 0 ) : QString();
|
|
QString style = result.PQntuples() == 1 ? result.PQgetvalue( 0, 1 ) : QString();
|
|
conn->unref();
|
|
|
|
QgsPostgresUtils::restoreInvalidXmlChars( style );
|
|
|
|
return style;
|
|
}
|
|
|
|
int QgsPostgresProviderMetadata::listStyles( const QString &uri, QStringList &ids, QStringList &names, QStringList &descriptions, QString &errCause )
|
|
{
|
|
errCause.clear();
|
|
QgsDataSourceUri dsUri( uri );
|
|
|
|
QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, true );
|
|
if ( !conn )
|
|
{
|
|
errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() );
|
|
return -1;
|
|
}
|
|
|
|
if ( !QgsPostgresUtils::tableExists( conn, QStringLiteral( "layer_styles" ) ) )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if ( dsUri.database().isEmpty() ) // typically when a service file is used
|
|
{
|
|
dsUri.setDatabase( conn->currentDatabase() );
|
|
}
|
|
|
|
QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
|
|
|
|
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 %4"
|
|
" AND (type=%5 OR type IS NULL)"
|
|
" ORDER BY useasdefault DESC, update_time DESC" )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
|
|
.arg( dsUri.geometryColumn().isEmpty() ? "f_geometry_column is NULL" : QString( "f_geometry_column=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
|
|
.arg( wkbTypeString );
|
|
|
|
QgsPostgresResult result( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), 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 AND type=%5)"
|
|
" ORDER BY update_time DESC" )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
|
|
.arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
|
|
.arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
|
|
.arg( wkbTypeString );
|
|
|
|
result = conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), 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;
|
|
}
|
|
|
|
bool QgsPostgresProviderMetadata::deleteStyleById( const QString &uri, const QString &styleId, QString &errCause )
|
|
{
|
|
QgsDataSourceUri dsUri( uri );
|
|
bool deleted;
|
|
|
|
QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, 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->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), deleteStyleQuery ) );
|
|
if ( result.PQresultStatus() != PGRES_COMMAND_OK )
|
|
{
|
|
QgsDebugError(
|
|
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;
|
|
}
|
|
|
|
QString QgsPostgresProviderMetadata::getStyleById( const QString &uri, const QString &styleId, QString &errCause )
|
|
{
|
|
QgsDataSourceUri dsUri( uri );
|
|
|
|
QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, true );
|
|
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->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), 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( QLatin1String( "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();
|
|
|
|
QgsPostgresUtils::restoreInvalidXmlChars( style );
|
|
|
|
return style;
|
|
}
|
|
|
|
QgsTransaction *QgsPostgresProviderMetadata::createTransaction( const QString &connString )
|
|
{
|
|
return new QgsPostgresTransaction( connString );
|
|
}
|
|
|
|
QMap<QString, QgsAbstractProviderConnection *> QgsPostgresProviderMetadata::connections( bool cached )
|
|
{
|
|
return connectionsProtected<QgsPostgresProviderConnection, QgsPostgresConn>( cached );
|
|
}
|
|
|
|
QgsAbstractProviderConnection *QgsPostgresProviderMetadata::createConnection( const QString &uri, const QVariantMap &configuration )
|
|
{
|
|
return new QgsPostgresProviderConnection( uri, configuration );
|
|
}
|
|
|
|
void QgsPostgresProviderMetadata::deleteConnection( const QString &name )
|
|
{
|
|
deleteConnectionProtected<QgsPostgresProviderConnection>( name );
|
|
}
|
|
|
|
void QgsPostgresProviderMetadata::saveConnection( const QgsAbstractProviderConnection *conn, const QString &name )
|
|
{
|
|
saveConnectionProtected( conn, name );
|
|
}
|
|
|
|
QgsAbstractProviderConnection *QgsPostgresProviderMetadata::createConnection( const QString &name )
|
|
{
|
|
return new QgsPostgresProviderConnection( name );
|
|
}
|
|
|
|
|
|
QgsPostgresProjectStorage *gPgProjectStorage = nullptr; // when not null it is owned by QgsApplication::projectStorageRegistry()
|
|
QgsPostgresLayerMetadataProvider *gPgLayerMetadataProvider = nullptr; // when not null it is owned by QgsApplication::layerMetadataProviderRegistry()
|
|
|
|
void QgsPostgresProviderMetadata::initProvider()
|
|
{
|
|
Q_ASSERT( !gPgProjectStorage );
|
|
gPgProjectStorage = new QgsPostgresProjectStorage;
|
|
QgsApplication::projectStorageRegistry()->registerProjectStorage( gPgProjectStorage ); // takes ownership
|
|
Q_ASSERT( !gPgLayerMetadataProvider );
|
|
gPgLayerMetadataProvider = new QgsPostgresLayerMetadataProvider();
|
|
QgsApplication::layerMetadataProviderRegistry()->registerLayerMetadataProvider( gPgLayerMetadataProvider ); // takes ownership
|
|
}
|
|
|
|
void QgsPostgresProviderMetadata::cleanupProvider()
|
|
{
|
|
QgsApplication::projectStorageRegistry()->unregisterProjectStorage( gPgProjectStorage ); // destroys the object
|
|
gPgProjectStorage = nullptr;
|
|
QgsApplication::layerMetadataProviderRegistry()->unregisterLayerMetadataProvider( gPgLayerMetadataProvider );
|
|
gPgLayerMetadataProvider = nullptr;
|
|
|
|
QgsPostgresConnPool::cleanupInstance();
|
|
}
|
|
|
|
|
|
QgsPostgresProviderMetadata::QgsPostgresProviderMetadata()
|
|
: QgsProviderMetadata( QgsPostgresProvider::POSTGRES_KEY, QgsPostgresProvider::POSTGRES_DESCRIPTION )
|
|
{
|
|
}
|
|
|
|
QIcon QgsPostgresProviderMetadata::icon() const
|
|
{
|
|
return QgsApplication::getThemeIcon( QStringLiteral( "mIconPostgis.svg" ) );
|
|
}
|
|
|
|
#ifndef HAVE_STATIC_PROVIDERS
|
|
QGISEXTERN QgsProviderMetadata *providerMetadataFactory()
|
|
{
|
|
return new QgsPostgresProviderMetadata();
|
|
}
|
|
#endif
|
|
|
|
|
|
QVariantMap QgsPostgresProviderMetadata::decodeUri( const QString &uri ) const
|
|
{
|
|
const QgsDataSourceUri dsUri { uri };
|
|
QVariantMap uriParts;
|
|
|
|
if ( !dsUri.database().isEmpty() )
|
|
uriParts[QStringLiteral( "dbname" )] = dsUri.database();
|
|
if ( !dsUri.host().isEmpty() )
|
|
uriParts[QStringLiteral( "host" )] = dsUri.host();
|
|
if ( !dsUri.port().isEmpty() )
|
|
uriParts[QStringLiteral( "port" )] = dsUri.port();
|
|
if ( !dsUri.service().isEmpty() )
|
|
uriParts[QStringLiteral( "service" )] = dsUri.service();
|
|
if ( !dsUri.username().isEmpty() )
|
|
uriParts[QStringLiteral( "username" )] = dsUri.username();
|
|
if ( !dsUri.password().isEmpty() )
|
|
uriParts[QStringLiteral( "password" )] = dsUri.password();
|
|
if ( !dsUri.authConfigId().isEmpty() )
|
|
uriParts[QStringLiteral( "authcfg" )] = dsUri.authConfigId();
|
|
if ( dsUri.wkbType() != Qgis::WkbType::Unknown )
|
|
uriParts[QStringLiteral( "type" )] = static_cast<quint32>( dsUri.wkbType() );
|
|
if ( uri.contains( QStringLiteral( "selectatid=" ), Qt::CaseSensitivity::CaseInsensitive ) )
|
|
uriParts[QStringLiteral( "selectatid" )] = !dsUri.selectAtIdDisabled();
|
|
if ( !dsUri.table().isEmpty() )
|
|
uriParts[QStringLiteral( "table" )] = dsUri.table();
|
|
if ( !dsUri.schema().isEmpty() )
|
|
uriParts[QStringLiteral( "schema" )] = dsUri.schema();
|
|
if ( !dsUri.keyColumn().isEmpty() )
|
|
uriParts[QStringLiteral( "key" )] = dsUri.keyColumn();
|
|
if ( !dsUri.srid().isEmpty() )
|
|
uriParts[QStringLiteral( "srid" )] = dsUri.srid();
|
|
if ( uri.contains( QStringLiteral( "estimatedmetadata=" ), Qt::CaseSensitivity::CaseInsensitive ) )
|
|
uriParts[QStringLiteral( "estimatedmetadata" )] = dsUri.useEstimatedMetadata();
|
|
if ( uri.contains( QStringLiteral( "sslmode=" ), Qt::CaseSensitivity::CaseInsensitive ) )
|
|
uriParts[QStringLiteral( "sslmode" )] = dsUri.sslMode();
|
|
if ( !dsUri.sql().isEmpty() )
|
|
uriParts[QStringLiteral( "sql" )] = dsUri.sql();
|
|
if ( !dsUri.param( QStringLiteral( "checkPrimaryKeyUnicity" ) ).isEmpty() )
|
|
uriParts[QStringLiteral( "checkPrimaryKeyUnicity" )] = dsUri.param( QStringLiteral( "checkPrimaryKeyUnicity" ) );
|
|
if ( !dsUri.geometryColumn().isEmpty() )
|
|
uriParts[QStringLiteral( "geometrycolumn" )] = dsUri.geometryColumn();
|
|
|
|
return uriParts;
|
|
}
|
|
|
|
QString QgsPostgresProviderMetadata::encodeUri( const QVariantMap &parts ) const
|
|
{
|
|
QgsDataSourceUri dsUri;
|
|
if ( parts.contains( QStringLiteral( "dbname" ) ) )
|
|
dsUri.setDatabase( parts.value( QStringLiteral( "dbname" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "port" ) ) )
|
|
dsUri.setParam( QStringLiteral( "port" ), parts.value( QStringLiteral( "port" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "host" ) ) )
|
|
dsUri.setParam( QStringLiteral( "host" ), parts.value( QStringLiteral( "host" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "service" ) ) )
|
|
dsUri.setParam( QStringLiteral( "service" ), parts.value( QStringLiteral( "service" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "username" ) ) )
|
|
dsUri.setUsername( parts.value( QStringLiteral( "username" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "password" ) ) )
|
|
dsUri.setPassword( parts.value( QStringLiteral( "password" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "authcfg" ) ) )
|
|
dsUri.setAuthConfigId( parts.value( QStringLiteral( "authcfg" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "type" ) ) )
|
|
dsUri.setParam( QStringLiteral( "type" ), QgsWkbTypes::displayString( static_cast<Qgis::WkbType>( parts.value( QStringLiteral( "type" ) ).toInt() ) ) );
|
|
if ( parts.contains( QStringLiteral( "selectatid" ) ) )
|
|
dsUri.setParam( QStringLiteral( "selectatid" ), parts.value( QStringLiteral( "selectatid" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "table" ) ) )
|
|
dsUri.setTable( parts.value( QStringLiteral( "table" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "schema" ) ) )
|
|
dsUri.setSchema( parts.value( QStringLiteral( "schema" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "key" ) ) )
|
|
dsUri.setParam( QStringLiteral( "key" ), parts.value( QStringLiteral( "key" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "srid" ) ) )
|
|
dsUri.setSrid( parts.value( QStringLiteral( "srid" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "estimatedmetadata" ) ) )
|
|
dsUri.setParam( QStringLiteral( "estimatedmetadata" ), parts.value( QStringLiteral( "estimatedmetadata" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "sslmode" ) ) )
|
|
dsUri.setParam( QStringLiteral( "sslmode" ), QgsDataSourceUri::encodeSslMode( static_cast<QgsDataSourceUri::SslMode>( parts.value( QStringLiteral( "sslmode" ) ).toInt() ) ) );
|
|
if ( parts.contains( QStringLiteral( "sql" ) ) )
|
|
dsUri.setSql( parts.value( QStringLiteral( "sql" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "checkPrimaryKeyUnicity" ) ) )
|
|
dsUri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), parts.value( QStringLiteral( "checkPrimaryKeyUnicity" ) ).toString() );
|
|
if ( parts.contains( QStringLiteral( "geometrycolumn" ) ) )
|
|
dsUri.setGeometryColumn( parts.value( QStringLiteral( "geometrycolumn" ) ).toString() );
|
|
return dsUri.uri( false );
|
|
}
|
|
|
|
QList<Qgis::LayerType> QgsPostgresProviderMetadata::supportedLayerTypes() const
|
|
{
|
|
return { Qgis::LayerType::Vector };
|
|
}
|
|
|
|
bool QgsPostgresProviderMetadata::saveLayerMetadata( const QString &uri, const QgsLayerMetadata &metadata, QString &errorMessage )
|
|
{
|
|
return QgsPostgresProviderMetadataUtils::saveLayerMetadata( Qgis::LayerType::Vector, uri, metadata, errorMessage );
|
|
}
|
|
|
|
|
|
QgsProviderMetadata::ProviderCapabilities QgsPostgresProviderMetadata::providerCapabilities() const
|
|
{
|
|
return QgsProviderMetadata::ProviderCapability::SaveLayerMetadata | QgsProviderMetadata::ProviderCapability::ParallelCreateProvider;
|
|
}
|