1 /***************************************************************************
2   qgspostgresprovider.cpp  -  QGIS data provider for PostgreSQL/PostGIS layers
3                              -------------------
4     begin                : 2004/01/07
5     copyright            : (C) 2004 by Gary E.Sherman
6     email                : sherman at mrcc.com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "qgsapplication.h"
19 #include "qgsfeature.h"
20 #include "qgsfield.h"
21 #include "qgsgeometry.h"
22 #include "qgsmessageoutput.h"
23 #include "qgsmessagelog.h"
24 #include "qgsprojectstorageregistry.h"
25 #include "qgsrectangle.h"
26 #include "qgscoordinatereferencesystem.h"
27 #include "qgsxmlutils.h"
28 #include "qgsvectorlayer.h"
29 #include "qgsvectorlayerexporter.h"
30 #include "qgspostgresprovider.h"
31 #include "qgspostgresconn.h"
32 #include "qgspostgresconnpool.h"
33 #include "qgspostgresdataitems.h"
34 #include "qgspostgresfeatureiterator.h"
35 #include "qgspostgrestransaction.h"
36 #include "qgspostgreslistener.h"
37 #include "qgspostgresprojectstorage.h"
38 #include "qgspostgresproviderconnection.h"
39 #include "qgslogger.h"
40 #include "qgsfeedback.h"
41 #include "qgssettings.h"
42 #include "qgsstringutils.h"
43 #include "qgsjsonutils.h"
44 
45 #include "qgspostgresprovider.h"
46 #include "qgsprovidermetadata.h"
47 #include "qgspostgresproviderconnection.h"
48 
49 #include <QMessageBox>
50 #include <QRegularExpression>
51 
52 const QString QgsPostgresProvider::POSTGRES_KEY = QStringLiteral( "postgres" );
53 const QString QgsPostgresProvider::POSTGRES_DESCRIPTION = QStringLiteral( "PostgreSQL/PostGIS data provider" );
54 
55 static const QString EDITOR_WIDGET_STYLES_TABLE = QStringLiteral( "qgis_editor_widget_styles" );
56 
PKINT2FID(qint32 x)57 inline qint64 PKINT2FID( qint32 x )
58 {
59   return QgsPostgresUtils::int32pk_to_fid( x );
60 }
61 
FID2PKINT(qint64 x)62 inline qint32 FID2PKINT( qint64 x )
63 {
64   return QgsPostgresUtils::fid_to_int32pk( x );
65 }
66 
tableExists(QgsPostgresConn & conn,const QString & name)67 static bool tableExists( QgsPostgresConn &conn, const QString &name )
68 {
69   QgsPostgresResult res( conn.PQexec( "SELECT EXISTS ( SELECT oid FROM pg_catalog.pg_class WHERE relname=" + QgsPostgresConn::quotedValue( name ) + ")" ) );
70   return res.PQgetvalue( 0, 0 ).startsWith( 't' );
71 }
72 
columnExists(QgsPostgresConn & conn,const QString & table,const QString & column)73 static bool columnExists( QgsPostgresConn &conn, const QString &table, const QString &column )
74 {
75   QgsPostgresResult res( conn.PQexec( "SELECT COUNT(*) FROM information_schema.columns WHERE table_name=" + QgsPostgresConn::quotedValue( table ) + " and column_name=" + QgsPostgresConn::quotedValue( column ) ) );
76   return res.PQgetvalue( 0, 0 ).toInt() > 0;
77 }
78 
79 QgsPostgresPrimaryKeyType
pkType(const QgsField & f) const80 QgsPostgresProvider::pkType( const QgsField &f ) const
81 {
82   switch ( f.type() )
83   {
84     case QVariant::LongLong:
85       // PostgreSQL doesn't have native "unsigned" types.
86       // Unsigned primary keys are emulated by the serial/bigserial
87       // pseudo-types, in which autogenerated values are always > 0;
88       // however, the database accepts manually inserted 0 and negative values
89       // in these fields.
90       return PktInt64;
91 
92     case QVariant::Int:
93       return PktInt;
94 
95     default:
96       return PktFidMap;
97   }
98 }
99 
100 
101 
QgsPostgresProvider(QString const & uri,const ProviderOptions & options,QgsDataProvider::ReadFlags flags)102 QgsPostgresProvider::QgsPostgresProvider( QString const &uri, const ProviderOptions &options,
103     QgsDataProvider::ReadFlags flags )
104   : QgsVectorDataProvider( uri, options, flags )
105   , mShared( new QgsPostgresSharedData )
106 {
107 
108   QgsDebugMsgLevel( QStringLiteral( "URI: %1 " ).arg( uri ), 2 );
109 
110   mUri = QgsDataSourceUri( uri );
111 
112   // populate members from the uri structure
113   mSchemaName = mUri.schema();
114   mTableName = mUri.table();
115   mGeometryColumn = mUri.geometryColumn();
116   mBoundingBoxColumn = mUri.param( "bbox" );
117   if ( mBoundingBoxColumn.isEmpty() )
118   {
119     mBoundingBoxColumn = mGeometryColumn;
120   }
121   mSqlWhereClause = mUri.sql();
122   mRequestedSrid = mUri.srid();
123   mRequestedGeomType = mUri.wkbType();
124 
125   const QString checkUnicityKey { QStringLiteral( "checkPrimaryKeyUnicity" ) };
126   if ( mUri.hasParam( checkUnicityKey ) )
127   {
128 
129     if ( mUri.param( checkUnicityKey ).compare( QLatin1String( "0" ) )  == 0 )
130     {
131       mCheckPrimaryKeyUnicity = false;
132     }
133     else
134     {
135       mCheckPrimaryKeyUnicity = true;
136     }
137     if ( mReadFlags & QgsDataProvider::FlagTrustDataSource )
138     {
139       mCheckPrimaryKeyUnicity = false;
140     }
141   }
142 
143   if ( mSchemaName.isEmpty() && mTableName.startsWith( '(' ) && mTableName.endsWith( ')' ) )
144   {
145     mIsQuery = true;
146     setQuery( mTableName );
147     mTableName.clear();
148   }
149   else
150   {
151     mIsQuery = false;
152 
153     setQuery( ( !mSchemaName.isEmpty() ? quotedIdentifier( mSchemaName ) + '.' : QString() )
154               + ( !mTableName.isEmpty() ? quotedIdentifier( mTableName ) : QString() ) );
155   }
156 
157   mUseEstimatedMetadata = mUri.useEstimatedMetadata();
158   if ( mReadFlags & QgsDataProvider::FlagTrustDataSource )
159   {
160     mUseEstimatedMetadata = true;
161   }
162   mSelectAtIdDisabled = mUri.selectAtIdDisabled();
163 
164   QgsDebugMsgLevel( QStringLiteral( "Connection info is %1" ).arg( mUri.connectionInfo( false ) ), 2 );
165   QgsDebugMsgLevel( QStringLiteral( "Geometry column is: %1" ).arg( mGeometryColumn ), 2 );
166   QgsDebugMsgLevel( QStringLiteral( "Schema is: %1" ).arg( mSchemaName ), 2 );
167   QgsDebugMsgLevel( QStringLiteral( "Table name is: %1" ).arg( mTableName ), 2 );
168   QgsDebugMsgLevel( QStringLiteral( "Query is: %1" ).arg( mQuery ), 2 );
169   QgsDebugMsgLevel( QStringLiteral( "Where clause is: %1" ).arg( mSqlWhereClause ), 2 );
170 
171   // no table/query passed, the provider could be used to get tables
172   if ( mQuery.isEmpty() )
173   {
174     return;
175   }
176 
177   mConnectionRO = QgsPostgresConn::connectDb( mUri.connectionInfo( false ), true );
178   if ( !mConnectionRO )
179   {
180     return;
181   }
182 
183   if ( !hasSufficientPermsAndCapabilities() ) // check permissions and set capabilities
184   {
185     disconnectDb();
186     return;
187   }
188 
189   if ( !getGeometryDetails() ) // gets srid, geometry and data type
190   {
191     // the table is not a geometry table
192     QgsMessageLog::logMessage( tr( "Invalid PostgreSQL layer" ), tr( "PostGIS" ) );
193     disconnectDb();
194     return;
195   }
196 
197   // NOTE: mValid would be true after true return from
198   // getGeometryDetails, see https://github.com/qgis/QGIS/issues/21807
199 
200   if ( mSpatialColType == SctTopoGeometry )
201   {
202     if ( !getTopoLayerInfo() ) // gets topology name and layer id
203     {
204       QgsMessageLog::logMessage( tr( "Invalid PostgreSQL topology layer" ), tr( "PostGIS" ) );
205       mValid = false;
206       disconnectDb();
207       return;
208     }
209   }
210 
211   mLayerExtent.setMinimal();
212 
213   // set the primary key
214   if ( !determinePrimaryKey() )
215   {
216     QgsMessageLog::logMessage( tr( "PostgreSQL layer has no primary key." ), tr( "PostGIS" ) );
217     mValid = false;
218     disconnectDb();
219     return;
220   }
221 
222   // Set the PostgreSQL message level so that we don't get the
223   // 'there is no transaction in progress' warning.
224 #ifndef QGISDEBUG
225   mConnectionRO->PQexecNR( QStringLiteral( "set client_min_messages to error" ) );
226 #endif
227 
228   setNativeTypes( mConnectionRO->nativeTypes() );
229 
230   QString key;
231   switch ( mPrimaryKeyType )
232   {
233     case PktOid:
234       key = QStringLiteral( "oid" );
235       break;
236     case PktTid:
237       key = QStringLiteral( "tid" );
238       break;
239     case PktInt:
240       Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
241       Q_ASSERT( mPrimaryKeyAttrs[0] >= 0 && mPrimaryKeyAttrs[0] < mAttributeFields.count() );
242       key = mAttributeFields.at( mPrimaryKeyAttrs.at( 0 ) ).name();
243       break;
244     case PktInt64:
245     case PktUint64:
246     case PktFidMap:
247     {
248       QString delim;
249       const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
250       for ( int idx : constMPrimaryKeyAttrs )
251       {
252         key += delim + mAttributeFields.at( idx ).name();
253         delim = ',';
254       }
255     }
256     break;
257     case PktUnknown:
258       QgsMessageLog::logMessage( tr( "PostgreSQL layer has unknown primary key type." ), tr( "PostGIS" ) );
259       mValid = false;
260       break;
261   }
262 
263   if ( mValid )
264   {
265     mUri.setKeyColumn( key );
266     setDataSourceUri( mUri.uri( false ) );
267   }
268   else
269   {
270     disconnectDb();
271   }
272 
273   mLayerMetadata.setType( QStringLiteral( "dataset" ) );
274   mLayerMetadata.setCrs( crs() );
275 }
276 
~QgsPostgresProvider()277 QgsPostgresProvider::~QgsPostgresProvider()
278 {
279   disconnectDb();
280 
281   QgsDebugMsgLevel( QStringLiteral( "deconstructing." ), 3 );
282 }
283 
284 
featureSource() const285 QgsAbstractFeatureSource *QgsPostgresProvider::featureSource() const
286 {
287   return new QgsPostgresFeatureSource( this );
288 }
289 
connectionRO() const290 QgsPostgresConn *QgsPostgresProvider::connectionRO() const
291 {
292   return mTransaction ? mTransaction->connection() : mConnectionRO;
293 }
294 
setListening(bool isListening)295 void QgsPostgresProvider::setListening( bool isListening )
296 {
297   if ( !mValid )
298     return;
299 
300   if ( isListening && !mListener )
301   {
302     mListener.reset( QgsPostgresListener::create( mUri.connectionInfo( false ) ).release() );
303     connect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify );
304   }
305   else if ( !isListening && mListener )
306   {
307     disconnect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify );
308     mListener.reset();
309   }
310 }
311 
reloadProviderData()312 void QgsPostgresProvider::reloadProviderData()
313 {
314   mShared->setFeaturesCounted( -1 );
315   mLayerExtent.setMinimal();
316 }
317 
318 
connectionRW()319 QgsPostgresConn *QgsPostgresProvider::connectionRW()
320 {
321   if ( mTransaction )
322   {
323     return mTransaction->connection();
324   }
325   else if ( !mConnectionRW )
326   {
327     mConnectionRW = QgsPostgresConn::connectDb( mUri.connectionInfo( false ), false );
328   }
329   return mConnectionRW;
330 }
331 
transaction() const332 QgsTransaction *QgsPostgresProvider::transaction() const
333 {
334   return mTransaction;
335 }
336 
providerKey()337 QString QgsPostgresProvider::providerKey()
338 {
339   return POSTGRES_KEY;
340 }
341 
setTransaction(QgsTransaction * transaction)342 void QgsPostgresProvider::setTransaction( QgsTransaction *transaction )
343 {
344   // static_cast since layers cannot be added to a transaction of a non-matching provider
345   mTransaction = static_cast<QgsPostgresTransaction *>( transaction );
346 }
347 
disconnectDb()348 void QgsPostgresProvider::disconnectDb()
349 {
350   if ( mConnectionRO )
351   {
352     mConnectionRO->unref();
353     mConnectionRO = nullptr;
354   }
355 
356   if ( mConnectionRW )
357   {
358     mConnectionRW->unref();
359     mConnectionRW = nullptr;
360   }
361 }
362 
quotedByteaValue(const QVariant & value)363 QString QgsPostgresProvider::quotedByteaValue( const QVariant &value )
364 {
365   if ( value.isNull() )
366     return QStringLiteral( "NULL" );
367 
368   const QByteArray ba = value.toByteArray();
369   const unsigned char *buf = reinterpret_cast< const unsigned char * >( ba.constData() );
370   QString param;
371   param.reserve( ba.length() * 4 );
372   for ( int i = 0; i < ba.length(); ++i )
373   {
374     param += QStringLiteral( "\\%1" ).arg( static_cast< int >( buf[i] ), 3, 8, QChar( '0' ) );
375   }
376   return QStringLiteral( "decode('%1','escape')" ).arg( param );
377 }
378 
storageType() const379 QString QgsPostgresProvider::storageType() const
380 {
381   return QStringLiteral( "PostgreSQL database with PostGIS extension" );
382 }
383 
getFeatures(const QgsFeatureRequest & request) const384 QgsFeatureIterator QgsPostgresProvider::getFeatures( const QgsFeatureRequest &request ) const
385 {
386   if ( !mValid )
387   {
388     QgsMessageLog::logMessage( tr( "Read attempt on an invalid PostgreSQL data source" ), tr( "PostGIS" ) );
389     return QgsFeatureIterator();
390   }
391 
392   QgsPostgresFeatureSource *featureSrc = static_cast<QgsPostgresFeatureSource *>( featureSource() );
393   return QgsFeatureIterator( new QgsPostgresFeatureIterator( featureSrc, true, request ) );
394 }
395 
396 
397 
pkParamWhereClause(int offset,const char * alias) const398 QString QgsPostgresProvider::pkParamWhereClause( int offset, const char *alias ) const
399 {
400   QString whereClause;
401 
402   QString aliased;
403   if ( alias ) aliased = QStringLiteral( "%1." ).arg( alias );
404 
405   switch ( mPrimaryKeyType )
406   {
407     case PktTid:
408       whereClause = QStringLiteral( "%2ctid=$%1" ).arg( offset ).arg( aliased );
409       break;
410 
411     case PktOid:
412       whereClause = QStringLiteral( "%2oid=$%1" ).arg( offset ).arg( aliased );
413       break;
414 
415     case PktInt:
416       Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
417       whereClause = QStringLiteral( "%3%1=$%2" ).arg( quotedIdentifier( field( mPrimaryKeyAttrs[0] ).name() ) ).arg( offset ).arg( aliased );
418       break;
419 
420     case PktInt64:
421     case PktUint64:
422     case PktFidMap:
423     {
424       QString delim;
425       for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ )
426       {
427         int idx = mPrimaryKeyAttrs[i];
428         QgsField fld = field( idx );
429 
430         whereClause += delim + QStringLiteral( "%3%1=$%2" ).arg( connectionRO()->fieldExpressionForWhereClause( fld ) ).arg( offset++ ).arg( aliased );
431         delim = QStringLiteral( " AND " );
432       }
433     }
434     break;
435 
436     case PktUnknown:
437       Q_ASSERT( !"FAILURE: Primary key unknown" );
438       whereClause = QStringLiteral( "NULL" );
439       break;
440   }
441 
442   if ( !mSqlWhereClause.isEmpty() )
443   {
444     if ( !whereClause.isEmpty() )
445       whereClause += QLatin1String( " AND " );
446 
447     whereClause += '(' + mSqlWhereClause + ')';
448   }
449 
450   return whereClause;
451 }
452 
appendPkParams(QgsFeatureId featureId,QStringList & params) const453 void QgsPostgresProvider::appendPkParams( QgsFeatureId featureId, QStringList &params ) const
454 {
455   switch ( mPrimaryKeyType )
456   {
457     case PktOid:
458       params << QString::number( featureId );
459       break;
460 
461     case PktInt:
462       params << QString::number( FID2PKINT( featureId ) );
463       break;
464 
465     case PktTid:
466       params << QStringLiteral( "'(%1,%2)'" ).arg( FID_TO_NUMBER( featureId ) >> 16 ).arg( FID_TO_NUMBER( featureId ) & 0xffff );
467       break;
468 
469     case PktInt64:
470     case PktUint64:
471     case PktFidMap:
472     {
473       QVariantList pkVals = mShared->lookupKey( featureId );
474       if ( !pkVals.isEmpty() )
475       {
476         Q_ASSERT( pkVals.size() == mPrimaryKeyAttrs.size() );
477       }
478 
479       for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ )
480       {
481         if ( i < pkVals.size() )
482         {
483           params << pkVals[i].toString();
484         }
485         else
486         {
487           QgsDebugMsg( QStringLiteral( "FAILURE: Key value %1 for feature %2 not found." ).arg( mPrimaryKeyAttrs[i] ).arg( featureId ) );
488           params << QStringLiteral( "NULL" );
489         }
490       }
491 
492       QgsDebugMsgLevel( QStringLiteral( "keys params: %1" ).arg( params.join( "; " ) ), 2 );
493     }
494     break;
495 
496     case PktUnknown:
497       Q_ASSERT( !"FAILURE: Primary key unknown" );
498       break;
499   }
500 }
501 
502 
whereClause(QgsFeatureId featureId) const503 QString QgsPostgresProvider::whereClause( QgsFeatureId featureId ) const
504 {
505   return QgsPostgresUtils::whereClause( featureId, mAttributeFields, connectionRO(), mPrimaryKeyType, mPrimaryKeyAttrs, mShared );
506 }
507 
whereClause(QgsFeatureIds featureIds) const508 QString QgsPostgresProvider::whereClause( QgsFeatureIds featureIds ) const
509 {
510   return QgsPostgresUtils::whereClause( featureIds, mAttributeFields, connectionRO(), mPrimaryKeyType, mPrimaryKeyAttrs, mShared );
511 }
512 
513 
whereClause(QgsFeatureId featureId,const QgsFields & fields,QgsPostgresConn * conn,QgsPostgresPrimaryKeyType pkType,const QList<int> & pkAttrs,const std::shared_ptr<QgsPostgresSharedData> & sharedData)514 QString QgsPostgresUtils::whereClause( QgsFeatureId featureId, const QgsFields &fields, QgsPostgresConn *conn, QgsPostgresPrimaryKeyType pkType, const QList<int> &pkAttrs, const std::shared_ptr<QgsPostgresSharedData> &sharedData )
515 {
516   QString whereClause;
517 
518   switch ( pkType )
519   {
520     case PktTid:
521       whereClause = QStringLiteral( "ctid='(%1,%2)'" )
522                     .arg( FID_TO_NUMBER( featureId ) >> 16 )
523                     .arg( FID_TO_NUMBER( featureId ) & 0xffff );
524       break;
525 
526     case PktOid:
527       whereClause = QStringLiteral( "oid=%1" ).arg( featureId );
528       break;
529 
530     case PktInt:
531       Q_ASSERT( pkAttrs.size() == 1 );
532       whereClause = QStringLiteral( "%1=%2" ).arg( QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) ).arg( FID2PKINT( featureId ) );
533       break;
534 
535     case PktInt64:
536     case PktUint64:
537     {
538       Q_ASSERT( pkAttrs.size() == 1 );
539       QVariantList pkVals = sharedData->lookupKey( featureId );
540       if ( !pkVals.isEmpty() )
541       {
542         QgsField fld = fields.at( pkAttrs[0] );
543         whereClause = conn->fieldExpression( fld );
544         if ( !pkVals[0].isNull() )
545           whereClause += '=' + pkVals[0].toString();
546         else
547           whereClause += QLatin1String( " IS NULL" );
548       }
549     }
550     break;
551 
552     case PktFidMap:
553     {
554       QVariantList pkVals = sharedData->lookupKey( featureId );
555       if ( !pkVals.isEmpty() )
556       {
557         Q_ASSERT( pkVals.size() == pkAttrs.size() );
558 
559         QString delim;
560         for ( int i = 0; i < pkAttrs.size(); i++ )
561         {
562           int idx = pkAttrs[i];
563           QgsField fld = fields.at( idx );
564 
565           whereClause += delim + conn->fieldExpressionForWhereClause( fld, pkVals[i].type() );
566           if ( pkVals[i].isNull() )
567             whereClause += QLatin1String( " IS NULL" );
568           else
569             whereClause += '=' + QgsPostgresConn::quotedValue( pkVals[i] ); // remove toString as it must be handled by quotedValue function
570 
571           delim = QStringLiteral( " AND " );
572         }
573       }
574       else
575       {
576         QgsDebugMsg( QStringLiteral( "FAILURE: Key values for feature %1 not found." ).arg( featureId ) );
577         whereClause = QStringLiteral( "NULL" );
578       }
579     }
580     break;
581 
582     case PktUnknown:
583       Q_ASSERT( !"FAILURE: Primary key unknown" );
584       whereClause = QStringLiteral( "NULL" );
585       break;
586   }
587 
588   return whereClause;
589 }
590 
whereClause(const QgsFeatureIds & featureIds,const QgsFields & fields,QgsPostgresConn * conn,QgsPostgresPrimaryKeyType pkType,const QList<int> & pkAttrs,const std::shared_ptr<QgsPostgresSharedData> & sharedData)591 QString QgsPostgresUtils::whereClause( const QgsFeatureIds &featureIds, const QgsFields &fields, QgsPostgresConn *conn, QgsPostgresPrimaryKeyType pkType, const QList<int> &pkAttrs, const std::shared_ptr<QgsPostgresSharedData> &sharedData )
592 {
593   auto lookupKeyWhereClause = [ = ]
594   {
595     if ( featureIds.isEmpty() )
596       return QString();
597 
598     //simple primary key, so prefer to use an "IN (...)" query. These are much faster then multiple chained ...OR... clauses
599     QString delim;
600     QString expr = QStringLiteral( "%1 IN (" ).arg( QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) );
601 
602     for ( const QgsFeatureId featureId : std::as_const( featureIds ) )
603     {
604       const QVariantList pkVals = sharedData->lookupKey( featureId );
605       if ( !pkVals.isEmpty() )
606       {
607         expr += delim + QgsPostgresConn::quotedValue( pkVals.at( 0 ) );
608         delim = ',';
609       }
610     }
611     expr += ')';
612 
613     return expr;
614   };
615 
616   switch ( pkType )
617   {
618     case PktOid:
619     case PktInt:
620     {
621       QString expr;
622 
623       //simple primary key, so prefer to use an "IN (...)" query. These are much faster then multiple chained ...OR... clauses
624       if ( !featureIds.isEmpty() )
625       {
626         QString delim;
627         expr = QStringLiteral( "%1 IN (" ).arg( ( pkType == PktOid ? QStringLiteral( "oid" ) : QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) ) );
628 
629         for ( const QgsFeatureId featureId : std::as_const( featureIds ) )
630         {
631           expr += delim + FID_TO_STRING( ( pkType == PktOid ? featureId : FID2PKINT( featureId ) ) );
632           delim = ',';
633         }
634         expr += ')';
635       }
636 
637       return expr;
638     }
639     case PktInt64:
640     case PktUint64:
641       return lookupKeyWhereClause();
642 
643     case PktFidMap:
644     case PktTid:
645     case PktUnknown:
646     {
647       // on simple string primary key we can use IN
648       if ( pkType == PktFidMap && pkAttrs.count() == 1 && fields.at( pkAttrs[0] ).type() == QVariant::String )
649         return lookupKeyWhereClause();
650 
651       //complex primary key, need to build up where string
652       QStringList whereClauses;
653       for ( const QgsFeatureId featureId : std::as_const( featureIds ) )
654       {
655         whereClauses << whereClause( featureId, fields, conn, pkType, pkAttrs, sharedData );
656       }
657       return whereClauses.isEmpty() ? QString() : whereClauses.join( QLatin1String( " OR " ) ).prepend( '(' ).append( ')' );
658     }
659   }
660   return QString(); //avoid warning
661 }
662 
andWhereClauses(const QString & c1,const QString & c2)663 QString QgsPostgresUtils::andWhereClauses( const QString &c1, const QString &c2 )
664 {
665   if ( c1.isEmpty() )
666     return c2;
667   if ( c2.isEmpty() )
668     return c1;
669 
670   return QStringLiteral( "(%1) AND (%2)" ).arg( c1, c2 );
671 }
672 
replaceInvalidXmlChars(QString & xml)673 void QgsPostgresUtils::replaceInvalidXmlChars( QString &xml )
674 {
675   static const QRegularExpression replaceRe { QStringLiteral( "([\x00-\x08\x0B-\x1F\x7F])" ) };
676   QRegularExpressionMatchIterator it {replaceRe.globalMatch( xml ) };
677   while ( it.hasNext() )
678   {
679     const QRegularExpressionMatch match { it.next() };
680     const QChar c { match.captured( 1 ).at( 0 ) };
681     xml.replace( c, QStringLiteral( "UTF-8[%1]" ).arg( c.unicode() ) );
682   }
683 }
684 
restoreInvalidXmlChars(QString & xml)685 void QgsPostgresUtils::restoreInvalidXmlChars( QString &xml )
686 {
687   static const QRegularExpression replaceRe { QStringLiteral( R"raw(UTF-8\[(\d+)\])raw" ) };
688   QRegularExpressionMatchIterator it {replaceRe.globalMatch( xml ) };
689   while ( it.hasNext() )
690   {
691     const QRegularExpressionMatch match { it.next() };
692     bool ok;
693     const ushort code { match.captured( 1 ).toUShort( &ok ) };
694     if ( ok )
695     {
696       xml.replace( QStringLiteral( "UTF-8[%1]" ).arg( code ), QChar( code ) );
697     }
698   }
699 }
700 
filterWhereClause() const701 QString QgsPostgresProvider::filterWhereClause() const
702 {
703   QString where;
704   QString delim = QStringLiteral( " WHERE " );
705 
706   if ( !mSqlWhereClause.isEmpty() )
707   {
708     where += delim + '(' + mSqlWhereClause + ')';
709     delim = QStringLiteral( " AND " );
710   }
711 
712   if ( !mRequestedSrid.isEmpty() && ( mRequestedSrid != mDetectedSrid || mRequestedSrid.toInt() == 0 ) )
713   {
714     where += delim + QStringLiteral( "%1(%2%3)=%4" )
715              .arg( connectionRO()->majorVersion() < 2 ? "srid" : "st_srid",
716                    quotedIdentifier( mGeometryColumn ),
717                    mSpatialColType == SctGeography ? "::geography" : "",
718                    mRequestedSrid );
719     delim = QStringLiteral( " AND " );
720   }
721 
722   if ( mRequestedGeomType != QgsWkbTypes::Unknown && mRequestedGeomType != mDetectedGeomType )
723   {
724     where += delim + QgsPostgresConn::postgisTypeFilter( mGeometryColumn, ( QgsWkbTypes::Type )mRequestedGeomType, mSpatialColType == SctGeography );
725     delim = QStringLiteral( " AND " );
726   }
727 
728   return where;
729 }
730 
setExtent(QgsRectangle & newExtent)731 void QgsPostgresProvider::setExtent( QgsRectangle &newExtent )
732 {
733   mLayerExtent.setXMaximum( newExtent.xMaximum() );
734   mLayerExtent.setXMinimum( newExtent.xMinimum() );
735   mLayerExtent.setYMaximum( newExtent.yMaximum() );
736   mLayerExtent.setYMinimum( newExtent.yMinimum() );
737 }
738 
739 /**
740  * Returns the feature type
741  */
wkbType() const742 QgsWkbTypes::Type QgsPostgresProvider::wkbType() const
743 {
744   return mRequestedGeomType != QgsWkbTypes::Unknown ? mRequestedGeomType : mDetectedGeomType;
745 }
746 
layerMetadata() const747 QgsLayerMetadata QgsPostgresProvider::layerMetadata() const
748 {
749   return mLayerMetadata;
750 }
751 
field(int index) const752 QgsField QgsPostgresProvider::field( int index ) const
753 {
754   if ( index < 0 || index >= mAttributeFields.count() )
755   {
756     QgsMessageLog::logMessage( tr( "FAILURE: Field %1 not found." ).arg( index ), tr( "PostGIS" ) );
757     throw PGFieldNotFound();
758   }
759 
760   return mAttributeFields.at( index );
761 }
762 
fields() const763 QgsFields QgsPostgresProvider::fields() const
764 {
765   return mAttributeFields;
766 }
767 
dataComment() const768 QString QgsPostgresProvider::dataComment() const
769 {
770   return mDataComment;
771 }
772 
773 
774 //! \todo XXX Perhaps this should be promoted to QgsDataProvider?
endianString()775 QString QgsPostgresProvider::endianString()
776 {
777   switch ( QgsApplication::endian() )
778   {
779     case QgsApplication::NDR:
780       return QStringLiteral( "NDR" );
781     case QgsApplication::XDR:
782       return QStringLiteral( "XDR" );
783     default :
784       return QStringLiteral( "Unknown" );
785   }
786 }
787 
788 
789 struct PGTypeInfo
790 {
791   QString typeName;
792   QString typeType;
793   QString typeElem;
794   int typeLen;
795 };
796 
loadFields()797 bool QgsPostgresProvider::loadFields()
798 {
799 
800   // Clear cached information about enum values support
801   mShared->clearSupportsEnumValuesCache();
802 
803   QString sql;
804   QString attroidsFilter;
805 
806   if ( !mIsQuery )
807   {
808     QgsDebugMsgLevel( QStringLiteral( "Loading fields for table %1" ).arg( mTableName ), 2 );
809 
810     // Get the table description
811     sql = QStringLiteral( "SELECT description FROM pg_description WHERE objoid=regclass(%1)::oid AND objsubid=0" ).arg( quotedValue( mQuery ) );
812     QgsPostgresResult tresult( connectionRO()->PQexec( sql ) );
813     if ( tresult.PQntuples() > 0 )
814     {
815       mDataComment = tresult.PQgetvalue( 0, 0 );
816       mLayerMetadata.setAbstract( mDataComment );
817     }
818   }
819 
820   // Populate the field vector for this layer. The field vector contains
821   // field name, type, length, and precision (if numeric)
822   sql = QStringLiteral( "SELECT * FROM %1 LIMIT 0" ).arg( mQuery );
823 
824   QgsPostgresResult result( connectionRO()->PQexec( sql ) );
825 
826   QMap<Oid, QMap<int, QString> > fmtFieldTypeMap, descrMap, defValMap, identityMap, generatedMap;
827   QMap<Oid, QMap<int, Oid> > attTypeIdMap;
828   QMap<Oid, QMap<int, bool> > notNullMap, uniqueMap;
829   if ( result.PQnfields() > 0 )
830   {
831     // Collect attribiute oids
832     QSet<Oid> attroids;
833     for ( int i = 0; i < result.PQnfields(); i++ )
834     {
835       Oid attroid = result.PQftype( i );
836       attroids.insert( attroid );
837     }
838 
839     // Collect table oids
840     QSet<Oid> tableoids;
841     for ( int i = 0; i < result.PQnfields(); i++ )
842     {
843       Oid tableoid = result.PQftable( i );
844       if ( tableoid > 0 )
845       {
846         tableoids.insert( tableoid );
847       }
848     }
849 
850     if ( !tableoids.isEmpty() )
851     {
852       QStringList tableoidsList;
853       const auto constTableoids = tableoids;
854       for ( Oid tableoid : constTableoids )
855       {
856         tableoidsList.append( QString::number( tableoid ) );
857       }
858 
859       QString tableoidsFilter = '(' + tableoidsList.join( QLatin1Char( ',' ) ) + ')';
860 
861       // Collect formatted field types
862       sql = QStringLiteral(
863               "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"
864               " FROM pg_attribute"
865               " LEFT OUTER JOIN pg_attrdef ON attrelid=adrelid AND attnum=adnum"
866 
867               // find unique constraints if present. Text cast required to handle int2vector comparison. Distinct required as multiple unique constraints may exist
868               " LEFT OUTER JOIN ( SELECT DISTINCT indrelid, indkey, indisunique FROM pg_index WHERE indisunique ) uniq ON attrelid=indrelid AND attnum::text=indkey::text "
869 
870               " WHERE attrelid IN %3"
871             ).arg( connectionRO()->pgVersion() >= 100000 ? QStringLiteral( ", attidentity" ) : QString(),
872                    connectionRO()->pgVersion() >= 120000 ? QStringLiteral( ", attgenerated" ) : QString(),
873                    tableoidsFilter );
874 
875       QgsPostgresResult fmtFieldTypeResult( connectionRO()->PQexec( sql ) );
876       for ( int i = 0; i < fmtFieldTypeResult.PQntuples(); ++i )
877       {
878         Oid attrelid = fmtFieldTypeResult.PQgetvalue( i, 0 ).toUInt();
879         int attnum = fmtFieldTypeResult.PQgetvalue( i, 1 ).toInt(); // Int2
880         QString formatType = fmtFieldTypeResult.PQgetvalue( i, 2 );
881         QString descr = fmtFieldTypeResult.PQgetvalue( i, 3 );
882         QString defVal = fmtFieldTypeResult.PQgetvalue( i, 4 );
883         Oid attType = fmtFieldTypeResult.PQgetvalue( i, 5 ).toUInt();
884         bool attNotNull = fmtFieldTypeResult.PQgetvalue( i, 6 ).toInt();
885         bool uniqueConstraint = fmtFieldTypeResult.PQgetvalue( i, 7 ).toInt();
886         QString attIdentity = connectionRO()->pgVersion() >= 100000 ? fmtFieldTypeResult.PQgetvalue( i, 8 ) : " ";
887 
888         // On PostgreSQL 12, the field pg_attribute.attgenerated is always filled with "s" if the field is generated,
889         // with the possibility of other values in future releases. This indicates "STORED" generated fields.
890         // The documentation for version 12 indicates that there is a future possibility of supporting virtual
891         // generated values, which might make possible to have values other than "s" on pg_attribute.attgenerated,
892         // which should be unimportant for QGIS if the user still won't be able to overwrite the column value.
893         // See https://www.postgresql.org/docs/12/ddl-generated-columns.html
894         QString attGenerated = connectionRO()->pgVersion() >= 120000 ? fmtFieldTypeResult.PQgetvalue( i, 9 ) : "";
895         fmtFieldTypeMap[attrelid][attnum] = formatType;
896         descrMap[attrelid][attnum] = descr;
897         defValMap[attrelid][attnum] = defVal;
898         attTypeIdMap[attrelid][attnum] = attType;
899         notNullMap[attrelid][attnum] = attNotNull;
900         uniqueMap[attrelid][attnum] = uniqueConstraint;
901         identityMap[attrelid][attnum] = attIdentity.isEmpty() ? " " : attIdentity;
902         generatedMap[attrelid][attnum] = attGenerated.isEmpty() ? QString() : defVal;
903 
904         // Also include atttype oid from pg_attribute, because PQnfields only returns basic type for for domains
905         attroids.insert( attType );
906       }
907     }
908 
909     // Prepare filter for fetching pg_type info
910     if ( !attroids.isEmpty() )
911     {
912       QStringList attroidsList;
913       for ( Oid attroid : std::as_const( attroids ) )
914       {
915         attroidsList.append( QString::number( attroid ) );
916       }
917       attroidsFilter = QStringLiteral( "WHERE oid in (%1)" ).arg( attroidsList.join( ',' ) );
918     }
919   }
920 
921   // Collect type info
922   sql = QStringLiteral( "SELECT oid,typname,typtype,typelem,typlen FROM pg_type %1" ).arg( attroidsFilter );
923   QgsPostgresResult typeResult( connectionRO()->PQexec( sql ) );
924 
925   QMap<Oid, PGTypeInfo> typeMap;
926   for ( int i = 0; i < typeResult.PQntuples(); ++i )
927   {
928     PGTypeInfo typeInfo =
929     {
930       /* typeName = */ typeResult.PQgetvalue( i, 1 ),
931       /* typeType = */ typeResult.PQgetvalue( i, 2 ),
932       /* typeElem = */ typeResult.PQgetvalue( i, 3 ),
933       /* typeLen = */ typeResult.PQgetvalue( i, 4 ).toInt()
934     };
935     typeMap.insert( typeResult.PQgetvalue( i, 0 ).toUInt(), typeInfo );
936   }
937 
938   QSet<QString> fields;
939   mAttributeFields.clear();
940   mIdentityFields.clear();
941   for ( int i = 0; i < result.PQnfields(); i++ )
942   {
943     QString fieldName = result.PQfname( i );
944     if ( fieldName == mGeometryColumn )
945       continue;
946 
947     Oid fldtyp = result.PQftype( i );
948     int fldMod = result.PQfmod( i );
949     int fieldPrec = 0;
950     Oid tableoid = result.PQftable( i );
951     int attnum = result.PQftablecol( i );
952     Oid atttypid = attTypeIdMap[tableoid][attnum];
953 
954     const PGTypeInfo &typeInfo = typeMap.value( fldtyp );
955     QString fieldTypeName = typeInfo.typeName;
956     QString fieldTType = typeInfo.typeType;
957     int fieldSize = typeInfo.typeLen;
958 
959     bool isDomain = ( typeMap.value( atttypid ).typeType == QLatin1String( "d" ) );
960 
961     QString formattedFieldType = fmtFieldTypeMap[tableoid][attnum];
962     QString originalFormattedFieldType = formattedFieldType;
963     if ( isDomain )
964     {
965       // get correct formatted field type for domain
966       sql = QStringLiteral( "SELECT format_type(%1, %2)" ).arg( fldtyp ).arg( fldMod );
967       QgsPostgresResult fmtFieldModResult( connectionRO()->PQexec( sql ) );
968       if ( fmtFieldModResult.PQntuples() > 0 )
969       {
970         formattedFieldType = fmtFieldModResult.PQgetvalue( 0, 0 );
971       }
972     }
973 
974     QString fieldComment = descrMap[tableoid][attnum];
975 
976     QVariant::Type fieldType = QVariant::Invalid;
977     QVariant::Type fieldSubType = QVariant::Invalid;
978 
979     if ( fieldTType == QLatin1String( "b" ) )
980     {
981       bool isArray = fieldTypeName.startsWith( '_' );
982 
983       if ( isArray )
984         fieldTypeName = fieldTypeName.mid( 1 );
985 
986       if ( fieldTypeName == QLatin1String( "int8" ) || fieldTypeName == QLatin1String( "serial8" ) )
987       {
988         fieldType = QVariant::LongLong;
989         fieldSize = -1;
990         fieldPrec = 0;
991       }
992       else if ( fieldTypeName == QLatin1String( "int2" ) || fieldTypeName == QLatin1String( "int4" ) ||
993                 fieldTypeName == QLatin1String( "oid" ) || fieldTypeName == QLatin1String( "serial" ) )
994       {
995         fieldType = QVariant::Int;
996         fieldSize = -1;
997         fieldPrec = 0;
998       }
999       else if ( fieldTypeName == QLatin1String( "real" ) || fieldTypeName == QLatin1String( "double precision" ) ||
1000                 fieldTypeName == QLatin1String( "float4" ) || fieldTypeName == QLatin1String( "float8" ) )
1001       {
1002         fieldType = QVariant::Double;
1003         fieldSize = -1;
1004         fieldPrec = 0;
1005       }
1006       else if ( fieldTypeName == QLatin1String( "numeric" ) )
1007       {
1008         fieldType = QVariant::Double;
1009 
1010         if ( formattedFieldType == QLatin1String( "numeric" ) || formattedFieldType.isEmpty() )
1011         {
1012           fieldSize = -1;
1013           fieldPrec = 0;
1014         }
1015         else
1016         {
1017           const QRegularExpression re( QRegularExpression::anchoredPattern( QStringLiteral( "numeric\\((\\d+),(\\d+)\\)" ) ) );
1018           const QRegularExpressionMatch match = re.match( formattedFieldType );
1019           if ( match.hasMatch() )
1020           {
1021             fieldSize = match.captured( 1 ).toInt();
1022             fieldPrec = match.captured( 2 ).toInt();
1023           }
1024           else if ( formattedFieldType != QLatin1String( "numeric" ) )
1025           {
1026             QgsMessageLog::logMessage( tr( "Unexpected formatted field type '%1' for field %2" )
1027                                        .arg( formattedFieldType,
1028                                              fieldName ),
1029                                        tr( "PostGIS" ) );
1030             fieldSize = -1;
1031             fieldPrec = 0;
1032           }
1033         }
1034       }
1035       else if ( fieldTypeName == QLatin1String( "varchar" ) )
1036       {
1037         fieldType = QVariant::String;
1038 
1039         const QRegularExpression re( QRegularExpression::anchoredPattern( "character varying\\((\\d+)\\)" ) );
1040         const QRegularExpressionMatch match = re.match( formattedFieldType );
1041         if ( match.hasMatch() )
1042         {
1043           fieldSize = match.captured( 1 ).toInt();
1044         }
1045         else
1046         {
1047           fieldSize = -1;
1048         }
1049       }
1050       else if ( fieldTypeName == QLatin1String( "date" ) )
1051       {
1052         fieldType = QVariant::Date;
1053         fieldSize = -1;
1054       }
1055       else if ( fieldTypeName == QLatin1String( "time" ) )
1056       {
1057         fieldType = QVariant::Time;
1058         fieldSize = -1;
1059       }
1060       else if ( fieldTypeName == QLatin1String( "timestamp" ) || fieldTypeName == QLatin1String( "timestamptz" ) )
1061       {
1062         fieldType = QVariant::DateTime;
1063         fieldSize = -1;
1064       }
1065       else if ( fieldTypeName == QLatin1String( "bytea" ) )
1066       {
1067         fieldType = QVariant::ByteArray;
1068         fieldSize = -1;
1069       }
1070       else if ( fieldTypeName == QLatin1String( "text" ) ||
1071                 fieldTypeName == QLatin1String( "citext" ) ||
1072                 fieldTypeName == QLatin1String( "geometry" ) ||
1073                 fieldTypeName == QLatin1String( "geography" ) ||
1074                 fieldTypeName == QLatin1String( "inet" ) ||
1075                 fieldTypeName == QLatin1String( "cidr" ) ||
1076                 fieldTypeName == QLatin1String( "macaddr" ) ||
1077                 fieldTypeName == QLatin1String( "macaddr8" ) ||
1078                 fieldTypeName == QLatin1String( "money" ) ||
1079                 fieldTypeName == QLatin1String( "ltree" ) ||
1080                 fieldTypeName == QLatin1String( "uuid" ) ||
1081                 fieldTypeName == QLatin1String( "xml" ) ||
1082                 fieldTypeName.startsWith( QLatin1String( "time" ) ) ||
1083                 fieldTypeName.startsWith( QLatin1String( "date" ) ) )
1084       {
1085         fieldType = QVariant::String;
1086         fieldSize = -1;
1087       }
1088       else if ( fieldTypeName == QLatin1String( "bpchar" ) )
1089       {
1090         // although postgres internally uses "bpchar", this is exposed to users as character in postgres
1091         fieldTypeName = QStringLiteral( "character" );
1092 
1093         fieldType = QVariant::String;
1094 
1095         const QRegularExpression re( QRegularExpression::anchoredPattern( "character\\((\\d+)\\)" ) );
1096         const QRegularExpressionMatch match = re.match( formattedFieldType );
1097         if ( match.hasMatch() )
1098         {
1099           fieldSize = match.captured( 1 ).toInt();
1100         }
1101         else
1102         {
1103           QgsDebugMsg( QStringLiteral( "Unexpected formatted field type '%1' for field %2" )
1104                        .arg( formattedFieldType,
1105                              fieldName ) );
1106           fieldSize = -1;
1107           fieldPrec = 0;
1108         }
1109       }
1110       else if ( fieldTypeName == QLatin1String( "char" ) )
1111       {
1112         fieldType = QVariant::String;
1113 
1114         const QRegularExpression re( QRegularExpression::anchoredPattern( QStringLiteral( "char\\((\\d+)\\)" ) ) );
1115         const QRegularExpressionMatch match = re.match( formattedFieldType );
1116         if ( match.hasMatch() )
1117         {
1118           fieldSize = match.captured( 1 ).toInt();
1119         }
1120         else
1121         {
1122           QgsMessageLog::logMessage( tr( "Unexpected formatted field type '%1' for field %2" )
1123                                      .arg( formattedFieldType,
1124                                            fieldName ) );
1125           fieldSize = -1;
1126           fieldPrec = 0;
1127         }
1128       }
1129       else if ( fieldTypeName == QLatin1String( "hstore" ) ||  fieldTypeName == QLatin1String( "json" ) || fieldTypeName == QLatin1String( "jsonb" ) )
1130       {
1131         fieldType = QVariant::Map;
1132         fieldSubType = QVariant::String;
1133         fieldSize = -1;
1134       }
1135       else if ( fieldTypeName == QLatin1String( "bool" ) )
1136       {
1137         // enum
1138         fieldType = QVariant::Bool;
1139         fieldSize = -1;
1140       }
1141       // PG 12 returns "name" type for some system table fields (e.g. information_schema.tables)
1142       else if ( fieldTypeName == QLatin1String( "name" ) )
1143       {
1144         fieldSubType = QVariant::String;
1145         fieldSize = 63;
1146       }
1147       else
1148       {
1149         // be tolerant in case of views: this might be a field used as a key
1150         const QgsPostgresProvider::Relkind type = relkind();
1151         if ( ( type == Relkind::View || type == Relkind::MaterializedView ) && parseUriKey( mUri.keyColumn( ) ).contains( fieldName ) )
1152         {
1153           // Assume it is convertible to text
1154           fieldType = QVariant::String;
1155           fieldSize = -1;
1156         }
1157         else if ( fieldTypeName == QLatin1String( "unknown" ) )
1158         {
1159           // Assume it is convertible to text
1160           fieldType = QVariant::String;
1161           fieldSize = -1;
1162         }
1163         else
1164         {
1165           QgsMessageLog::logMessage( tr( "Field %1 ignored, because of unsupported type %2" ).arg( fieldName, fieldTType ), tr( "PostGIS" ) );
1166           continue;
1167         }
1168       }
1169 
1170       if ( isArray )
1171       {
1172         fieldTypeName = '_' + fieldTypeName;
1173         fieldSubType = fieldType;
1174         fieldType = ( fieldType == QVariant::String ? QVariant::StringList : QVariant::List );
1175         fieldSize = -1;
1176       }
1177     }
1178     else if ( fieldTType == QLatin1String( "e" ) )
1179     {
1180       // enum
1181       fieldType = QVariant::String;
1182       fieldSize = -1;
1183     }
1184     else
1185     {
1186       QgsMessageLog::logMessage( tr( "Field %1 ignored, because of unsupported type %2" ).arg( fieldName, fieldTType ), tr( "PostGIS" ) );
1187       continue;
1188     }
1189 
1190     if ( fields.contains( fieldName ) )
1191     {
1192       QgsMessageLog::logMessage( tr( "Duplicate field %1 found\n" ).arg( fieldName ), tr( "PostGIS" ) );
1193       // In case of read-only query layers we can safely ignore the issue and rename the duplicated field
1194       if ( ! mIsQuery )
1195       {
1196         return false;
1197       }
1198       else
1199       {
1200         unsigned short int i = 1;
1201         while ( i < std::numeric_limits<unsigned short int>::max() )
1202         {
1203           const QString newName { QStringLiteral( "%1 (%2)" ).arg( fieldName ).arg( ++i ) };
1204           if ( ! fields.contains( newName ) )
1205           {
1206             fieldName = newName;
1207             break;
1208           }
1209         }
1210       }
1211     }
1212 
1213     fields << fieldName;
1214 
1215     if ( isDomain )
1216     {
1217       //field was defined using domain, so use domain type name for fieldTypeName
1218       fieldTypeName = originalFormattedFieldType;
1219     }
1220 
1221     mAttrPalIndexName.insert( i, fieldName );
1222     // If this is an identity field with constraints and there is no default, let's look for a sequence:
1223     // we might have a default value created by a sequence named <table>_<field>_seq
1224     if ( ! identityMap[tableoid ][ attnum ].isEmpty()
1225          && notNullMap[tableoid][ attnum ]
1226          && uniqueMap[tableoid][attnum]
1227          && defValMap[tableoid][attnum].isEmpty() )
1228     {
1229       const QString seqName { mTableName + '_' + fieldName + QStringLiteral( "_seq" ) };
1230       const QString seqSql = QStringLiteral( "SELECT c.oid "
1231                                              "  FROM pg_class c "
1232                                              "  LEFT JOIN pg_namespace n "
1233                                              "    ON ( n.oid = c.relnamespace ) "
1234                                              "  WHERE c.relkind = 'S' "
1235                                              "    AND c.relname = %1 "
1236                                              "    AND n.nspname = %2" )
1237                              .arg( quotedValue( seqName ) )
1238                              .arg( quotedValue( mSchemaName ) );
1239       QgsPostgresResult seqResult( connectionRO()->PQexec( seqSql ) );
1240       if ( seqResult.PQntuples() == 1 )
1241       {
1242         defValMap[tableoid][attnum] = QStringLiteral( "nextval(%1::regclass)" ).arg( quotedValue( seqName ) );
1243       }
1244     }
1245 
1246     mDefaultValues.insert( mAttributeFields.size(), defValMap[tableoid][attnum] );
1247 
1248     const QString generatedValue = generatedMap[tableoid][attnum];
1249     if ( !generatedValue.isNull() )
1250       mGeneratedValues.insert( mAttributeFields.size(), generatedValue );
1251 
1252     QgsField newField = QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment, fieldSubType );
1253     newField.setReadOnly( !generatedValue.isNull() );
1254 
1255     QgsFieldConstraints constraints;
1256     if ( notNullMap[tableoid][attnum] || ( mPrimaryKeyAttrs.size() == 1 && mPrimaryKeyAttrs[0] == i ) || identityMap[tableoid][attnum] != ' ' )
1257       constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
1258     if ( uniqueMap[tableoid][attnum] || ( mPrimaryKeyAttrs.size() == 1 && mPrimaryKeyAttrs[0] == i ) || identityMap[tableoid][attnum] != ' ' )
1259       constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
1260     newField.setConstraints( constraints );
1261 
1262     mIdentityFields.insert( mAttributeFields.size(), identityMap[tableoid][attnum][0].toLatin1() );
1263     mAttributeFields.append( newField );
1264   }
1265 
1266   setEditorWidgets();
1267 
1268   return true;
1269 }
1270 
setEditorWidgets()1271 void QgsPostgresProvider::setEditorWidgets()
1272 {
1273   if ( ! tableExists( *connectionRO(), EDITOR_WIDGET_STYLES_TABLE ) )
1274   {
1275     return;
1276   }
1277 
1278   QStringList quotedFnames;
1279   const QStringList fieldNames = mAttributeFields.names();
1280   for ( const QString &name : fieldNames )
1281   {
1282     quotedFnames << quotedValue( name );
1283   }
1284 
1285   // We expect the table to be created like this:
1286   //
1287   // CREATE TABLE qgis_editor_widget_styles (schema_name TEXT NOT NULL, table_name TEXT NOT NULL, field_name TEXT NOT NULL,
1288   //                                         type TEXT NOT NULL, config TEXT,
1289   //                                         PRIMARY KEY(schema_name, table_name, field_name));
1290   const QString sql = QStringLiteral( "SELECT field_name, type, config "
1291                                       "FROM %1 WHERE schema_name = %2 "
1292                                       "AND table_name = %3 "
1293                                       "AND field_name IN ( %4 )" ) .
1294                       arg( EDITOR_WIDGET_STYLES_TABLE, quotedValue( mSchemaName ),
1295                            quotedValue( mTableName ), quotedFnames.join( "," ) );
1296   QgsPostgresResult result( connectionRO()->PQexec( sql ) );
1297   for ( int i = 0; i < result.PQntuples(); ++i )
1298   {
1299     if ( result.PQgetisnull( i, 2 ) ) continue; // config can be null and it's OK
1300 
1301     const QString &configTxt = result.PQgetvalue( i, 2 );
1302     const QString &type = result.PQgetvalue( i, 1 );
1303     const QString &fname = result.PQgetvalue( i, 0 );
1304     QVariantMap config;
1305     QDomDocument doc;
1306     if ( doc.setContent( configTxt ) )
1307     {
1308       config = QgsXmlUtils::readVariant( doc.documentElement() ).toMap();
1309     }
1310     else
1311     {
1312       QgsMessageLog::logMessage(
1313         tr( "Cannot parse widget configuration for field %1.%2.%3\n" )
1314         .arg( mSchemaName, mTableName, fname ), tr( "PostGIS" )
1315       );
1316       continue;
1317     }
1318 
1319     // Set corresponding editor widget
1320     for ( auto &field : mAttributeFields )
1321     {
1322       if ( field.name() == fname )
1323       {
1324         field.setEditorWidgetSetup( QgsEditorWidgetSetup( type, config ) );
1325         break;
1326       }
1327     }
1328   }
1329 }
1330 
hasSufficientPermsAndCapabilities()1331 bool QgsPostgresProvider::hasSufficientPermsAndCapabilities()
1332 {
1333   QgsDebugMsgLevel( QStringLiteral( "Checking for permissions on the relation" ), 2 );
1334 
1335   mEnabledCapabilities = QgsVectorDataProvider::Capability::ReloadData;
1336 
1337   QgsPostgresResult testAccess;
1338   if ( !mIsQuery )
1339   {
1340     // Check that we can read from the table (i.e., we have select permission).
1341     QString sql = QStringLiteral( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery );
1342     QgsPostgresResult testAccess( connectionRO()->PQexec( sql ) );
1343     if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK )
1344     {
1345       QgsMessageLog::logMessage( tr( "Unable to access the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" )
1346                                  .arg( mQuery,
1347                                        testAccess.PQresultErrorMessage(),
1348                                        sql ), tr( "PostGIS" ) );
1349       return false;
1350     }
1351 
1352     bool inRecovery = false;
1353 
1354     if ( connectionRO()->pgVersion() >= 90000 )
1355     {
1356       testAccess = connectionRO()->PQexec( QStringLiteral( "SELECT pg_is_in_recovery()" ) );
1357       if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK || testAccess.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) )
1358       {
1359         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" ) );
1360         inRecovery = true;
1361       }
1362     }
1363 
1364     // postgres has fast access to features at id (thanks to primary key / unique index)
1365     // the latter flag is here just for compatibility
1366     if ( !mSelectAtIdDisabled )
1367     {
1368       mEnabledCapabilities |= QgsVectorDataProvider::SelectAtId;
1369     }
1370 
1371     if ( !inRecovery )
1372     {
1373       if ( connectionRO()->pgVersion() >= 80400 )
1374       {
1375         sql = QString( "SELECT "
1376                        "has_table_privilege(%1,'DELETE'),"
1377                        "has_any_column_privilege(%1,'UPDATE'),"
1378                        "%2"
1379                        "has_table_privilege(%1,'INSERT'),"
1380                        "current_schema()" )
1381               .arg( quotedValue( mQuery ),
1382                     mGeometryColumn.isNull()
1383                     ? QStringLiteral( "'f'," )
1384                     : QStringLiteral( "has_column_privilege(%1,%2,'UPDATE')," )
1385                     .arg( quotedValue( mQuery ),
1386                           quotedValue( mGeometryColumn ) )
1387                   );
1388       }
1389       else
1390       {
1391         sql = QString( "SELECT "
1392                        "has_table_privilege(%1,'DELETE'),"
1393                        "has_table_privilege(%1,'UPDATE'),"
1394                        "has_table_privilege(%1,'UPDATE'),"
1395                        "has_table_privilege(%1,'INSERT'),"
1396                        "current_schema()" )
1397               .arg( quotedValue( mQuery ) );
1398       }
1399 
1400       testAccess = connectionRO()->PQexec( sql );
1401       if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK )
1402       {
1403         QgsMessageLog::logMessage( tr( "Unable to determine table access privileges for the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" )
1404                                    .arg( mQuery,
1405                                          testAccess.PQresultErrorMessage(),
1406                                          sql ),
1407                                    tr( "PostGIS" ) );
1408         return false;
1409       }
1410 
1411 
1412       if ( testAccess.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) )
1413       {
1414         // DELETE
1415         mEnabledCapabilities |= QgsVectorDataProvider::DeleteFeatures | QgsVectorDataProvider::FastTruncate;
1416       }
1417 
1418       if ( testAccess.PQgetvalue( 0, 1 ) == QLatin1String( "t" ) )
1419       {
1420         // UPDATE
1421         mEnabledCapabilities |= QgsVectorDataProvider::ChangeAttributeValues;
1422       }
1423 
1424       if ( testAccess.PQgetvalue( 0, 2 ) == QLatin1String( "t" ) )
1425       {
1426         // UPDATE
1427         mEnabledCapabilities |= QgsVectorDataProvider::ChangeGeometries;
1428       }
1429 
1430       if ( testAccess.PQgetvalue( 0, 3 ) == QLatin1String( "t" ) )
1431       {
1432         // INSERT
1433         mEnabledCapabilities |= QgsVectorDataProvider::AddFeatures;
1434       }
1435 
1436       if ( mSchemaName.isEmpty() )
1437         mSchemaName = testAccess.PQgetvalue( 0, 4 );
1438 
1439       sql = QString( "SELECT 1 FROM pg_class,pg_namespace WHERE "
1440                      "pg_class.relnamespace=pg_namespace.oid AND "
1441                      "%3 AND "
1442                      "relname=%1 AND nspname=%2" )
1443             .arg( quotedValue( mTableName ),
1444                   quotedValue( mSchemaName ),
1445                   connectionRO()->pgVersion() < 80100 ? "pg_get_userbyid(relowner)=current_user" : "pg_has_role(relowner,'MEMBER')" );
1446       testAccess = connectionRO()->PQexec( sql );
1447       if ( testAccess.PQresultStatus() == PGRES_TUPLES_OK && testAccess.PQntuples() == 1 )
1448       {
1449         mEnabledCapabilities |= QgsVectorDataProvider::AddAttributes | QgsVectorDataProvider::DeleteAttributes | QgsVectorDataProvider::RenameAttributes;
1450       }
1451     }
1452   }
1453   else
1454   {
1455     // Check if the sql is a select query
1456     if ( !mQuery.startsWith( '(' ) && !mQuery.endsWith( ')' ) )
1457     {
1458       QgsMessageLog::logMessage( tr( "The custom query is not a select query." ), tr( "PostGIS" ) );
1459       return false;
1460     }
1461 
1462     // get a new alias for the subquery
1463     int index = 0;
1464     QString alias;
1465     QRegularExpression regex;
1466     do
1467     {
1468       alias = QStringLiteral( "subQuery_%1" ).arg( QString::number( index++ ) );
1469       QString pattern = QStringLiteral( "(\\\"?)%1\\1" ).arg( QgsStringUtils::qRegExpEscape( alias ) );
1470       regex.setPattern( pattern );
1471       regex.setPatternOptions( QRegularExpression::CaseInsensitiveOption );
1472     }
1473     while ( mQuery.contains( regex ) );
1474 
1475     // convert the custom query into a subquery
1476     setQuery( QStringLiteral( "%1 AS %2" )
1477               .arg( mQuery,
1478                     quotedIdentifier( alias ) ) );
1479 
1480     QString sql = QStringLiteral( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery );
1481 
1482     testAccess = connectionRO()->PQexec( sql );
1483     if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK )
1484     {
1485       QgsMessageLog::logMessage( tr( "Unable to execute the query.\nThe error message from the database was:\n%1.\nSQL: %2" )
1486                                  .arg( testAccess.PQresultErrorMessage(),
1487                                        sql ), tr( "PostGIS" ) );
1488       return false;
1489     }
1490 
1491     if ( !mSelectAtIdDisabled )
1492     {
1493       mEnabledCapabilities = QgsVectorDataProvider::SelectAtId;
1494     }
1495   }
1496 
1497   // supports geometry simplification on provider side
1498   mEnabledCapabilities |= ( QgsVectorDataProvider::SimplifyGeometries | QgsVectorDataProvider::SimplifyGeometriesWithTopologicalValidation );
1499 
1500   //supports transactions
1501   mEnabledCapabilities |= QgsVectorDataProvider::TransactionSupport;
1502 
1503   // supports circular geometries
1504   mEnabledCapabilities |= QgsVectorDataProvider::CircularGeometries;
1505 
1506   // supports layer metadata
1507   mEnabledCapabilities |= QgsVectorDataProvider::ReadLayerMetadata;
1508 
1509   if ( ( mEnabledCapabilities & QgsVectorDataProvider::ChangeGeometries ) &&
1510        ( mEnabledCapabilities & QgsVectorDataProvider::ChangeAttributeValues ) &&
1511        mSpatialColType != SctTopoGeometry )
1512   {
1513     mEnabledCapabilities |= QgsVectorDataProvider::ChangeFeatures;
1514   }
1515 
1516   return true;
1517 }
1518 
determinePrimaryKey()1519 bool QgsPostgresProvider::determinePrimaryKey()
1520 {
1521   if ( !loadFields() )
1522   {
1523     return false;
1524   }
1525 
1526   // check to see if there is an unique index on the relation, which
1527   // can be used as a key into the table. Primary keys are always
1528   // unique indices, so we catch them as well.
1529 
1530   QString sql;
1531   if ( !mIsQuery )
1532   {
1533     sql = QStringLiteral( "SELECT count(*) FROM pg_inherits WHERE inhparent=%1::regclass" ).arg( quotedValue( mQuery ) );
1534     QgsDebugMsgLevel( QStringLiteral( "Checking whether %1 is a parent table" ).arg( sql ), 2 );
1535     QgsPostgresResult res( connectionRO()->PQexec( sql ) );
1536     bool isParentTable( res.PQntuples() == 0 || res.PQgetvalue( 0, 0 ).toInt() > 0 );
1537 
1538     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 ) );
1539     QgsDebugMsgLevel( QStringLiteral( "Retrieving first primary or unique index: %1" ).arg( sql ), 2 );
1540 
1541     res = connectionRO()->PQexec( sql );
1542     QgsDebugMsgLevel( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ), 2 );
1543 
1544     QStringList log;
1545 
1546     // no primary or unique indices found
1547     if ( res.PQntuples() == 0 )
1548     {
1549       QgsDebugMsgLevel( QStringLiteral( "Relation has no primary key -- investigating alternatives" ), 2 );
1550 
1551       // Two options here. If the relation is a table, see if there is
1552       // an oid column that can be used instead.
1553       // If the relation is a view try to find a suitable column to use as
1554       // the primary key.
1555 
1556       const QgsPostgresProvider::Relkind type = relkind();
1557 
1558       if ( type == Relkind::OrdinaryTable || type == Relkind::PartitionedTable )
1559       {
1560         QgsDebugMsgLevel( QStringLiteral( "Relation is a table. Checking to see if it has an oid column." ), 2 );
1561 
1562         mPrimaryKeyAttrs.clear();
1563         mPrimaryKeyType = PktUnknown;
1564 
1565         if ( connectionRO()->pgVersion() >= 100000 )
1566         {
1567           // If there is an generated id on the table, use that instead,
1568           sql = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attidentity IN ('a','d') AND attrelid=regclass(%1) LIMIT 1" ).arg( quotedValue( mQuery ) );
1569           res = connectionRO()->PQexec( sql );
1570           if ( res.PQntuples() == 1 )
1571           {
1572             // Could warn the user here that performance will suffer if
1573             // attribute isn't indexed (and that they may want to add a
1574             // primary key to the table)
1575             int idx = fieldNameIndex( res.PQgetvalue( 0, 0 ) );
1576             mPrimaryKeyType = pkType( mAttributeFields.at( idx ) );
1577             mPrimaryKeyAttrs << idx;
1578           }
1579         }
1580 
1581         if ( mPrimaryKeyType == PktUnknown )
1582         {
1583           // If there is an oid on the table, use that instead,
1584           sql = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attname='oid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) );
1585 
1586           res = connectionRO()->PQexec( sql );
1587           if ( res.PQntuples() == 1 )
1588           {
1589             // Could warn the user here that performance will suffer if
1590             // oid isn't indexed (and that they may want to add a
1591             // primary key to the table)
1592             mPrimaryKeyType = PktOid;
1593           }
1594         }
1595 
1596         if ( mPrimaryKeyType == PktUnknown )
1597         {
1598           sql = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attname='ctid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) );
1599 
1600           res = connectionRO()->PQexec( sql );
1601           if ( res.PQntuples() == 1 )
1602           {
1603             mPrimaryKeyType = PktTid;
1604 
1605             QgsMessageLog::logMessage( tr( "Primary key is ctid - changing of existing features disabled (%1; %2)" ).arg( mGeometryColumn, mQuery ) );
1606             mEnabledCapabilities &= ~( QgsVectorDataProvider::DeleteFeatures | QgsVectorDataProvider::ChangeAttributeValues | QgsVectorDataProvider::ChangeGeometries | QgsVectorDataProvider::ChangeFeatures );
1607           }
1608         }
1609 
1610         if ( mPrimaryKeyType == PktUnknown )
1611         {
1612           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" ) );
1613         }
1614       }
1615       else if ( type == Relkind::View || type == Relkind::MaterializedView || type == Relkind::ForeignTable )
1616       {
1617         determinePrimaryKeyFromUriKeyColumn();
1618       }
1619       else
1620       {
1621         const QMetaEnum metaEnum( QMetaEnum::fromType<Relkind>() );
1622         QString typeName = metaEnum.valueToKey( type );
1623         QgsMessageLog::logMessage( tr( "Unexpected relation type '%1'." ).arg( typeName ), tr( "PostGIS" ) );
1624       }
1625     }
1626     else
1627     {
1628       // have a primary key or unique index
1629       QString indrelid = res.PQgetvalue( 0, 0 );
1630       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 );
1631 
1632       QgsDebugMsgLevel( "Retrieving key columns: " + sql, 2 );
1633       res = connectionRO()->PQexec( sql );
1634       QgsDebugMsgLevel( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ), 2 );
1635 
1636       bool mightBeNull = false;
1637       QString primaryKey;
1638       QString delim;
1639 
1640       mPrimaryKeyType = PktFidMap; // map by default, will downgrade if needed
1641       for ( int i = 0; i < res.PQntuples(); i++ )
1642       {
1643         QString name = res.PQgetvalue( i, 0 );
1644         if ( res.PQgetvalue( i, 1 ).startsWith( 'f' ) )
1645         {
1646           QgsMessageLog::logMessage( tr( "Unique column '%1' doesn't have a NOT NULL constraint." ).arg( name ), tr( "PostGIS" ) );
1647           mightBeNull = true;
1648         }
1649 
1650         primaryKey += delim + quotedIdentifier( name );
1651         delim = ',';
1652 
1653         int idx = fieldNameIndex( name );
1654         if ( idx == -1 )
1655         {
1656           QgsDebugMsgLevel( "Skipping " + name, 2 );
1657           continue;
1658         }
1659         QgsField fld = mAttributeFields.at( idx );
1660 
1661         // Always use PktFidMap for multi-field keys
1662         mPrimaryKeyType = i ? PktFidMap : pkType( fld );
1663 
1664         mPrimaryKeyAttrs << idx;
1665       }
1666 
1667       if ( ( mightBeNull || isParentTable ) && !mUseEstimatedMetadata && !uniqueData( primaryKey ) )
1668       {
1669         QgsMessageLog::logMessage( tr( "Ignoring key candidate because of NULL values or inheritance" ), tr( "PostGIS" ) );
1670         mPrimaryKeyType = PktUnknown;
1671         mPrimaryKeyAttrs.clear();
1672       }
1673     }
1674   }
1675   else
1676   {
1677     determinePrimaryKeyFromUriKeyColumn();
1678   }
1679 
1680   if ( mPrimaryKeyAttrs.size() == 1 )
1681   {
1682     //primary keys are unique, not null
1683     QgsFieldConstraints constraints = mAttributeFields.at( mPrimaryKeyAttrs[0] ).constraints();
1684     constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
1685     constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
1686     mAttributeFields[ mPrimaryKeyAttrs[0] ].setConstraints( constraints );
1687   }
1688 
1689   mValid = mPrimaryKeyType != PktUnknown;
1690 
1691   return mValid;
1692 }
1693 
1694 /* static */
parseUriKey(const QString & key)1695 QStringList QgsPostgresProvider::parseUriKey( const QString &key )
1696 {
1697   if ( key.isEmpty() ) return QStringList();
1698 
1699   QStringList cols;
1700 
1701   // remove quotes from key list
1702   if ( key.startsWith( '"' ) && key.endsWith( '"' ) )
1703   {
1704     int i = 1;
1705     QString col;
1706     while ( i < key.size() )
1707     {
1708       if ( key[i] == '"' )
1709       {
1710         if ( i + 1 < key.size() && key[i + 1] == '"' )
1711         {
1712           i++;
1713         }
1714         else
1715         {
1716           cols << col;
1717           col.clear();
1718 
1719           if ( ++i == key.size() )
1720             break;
1721 
1722           Q_ASSERT( key[i] == ',' );
1723           i++;
1724           Q_ASSERT( key[i] == '"' );
1725           i++;
1726           col.clear();
1727           continue;
1728         }
1729       }
1730 
1731       col += key[i++];
1732     }
1733   }
1734   else if ( key.contains( ',' ) )
1735   {
1736     cols = key.split( ',' );
1737   }
1738   else
1739   {
1740     cols << key;
1741   }
1742 
1743   return cols;
1744 }
1745 
determinePrimaryKeyFromUriKeyColumn()1746 void QgsPostgresProvider::determinePrimaryKeyFromUriKeyColumn()
1747 {
1748   QString primaryKey = mUri.keyColumn();
1749   mPrimaryKeyType = PktUnknown;
1750 
1751   if ( !primaryKey.isEmpty() )
1752   {
1753     const QStringList cols = parseUriKey( primaryKey );
1754 
1755     primaryKey.clear();
1756     QString del;
1757     for ( const QString &col : cols )
1758     {
1759       primaryKey += del + quotedIdentifier( col );
1760       del = QStringLiteral( "," );
1761     }
1762 
1763     for ( const QString &col : cols )
1764     {
1765       int idx = fieldNameIndex( col );
1766       if ( idx < 0 )
1767       {
1768         QgsMessageLog::logMessage( tr( "Key field '%1' for view/query not found." ).arg( col ), tr( "PostGIS" ) );
1769         mPrimaryKeyAttrs.clear();
1770         break;
1771       }
1772 
1773       mPrimaryKeyAttrs << idx;
1774     }
1775 
1776     if ( !mPrimaryKeyAttrs.isEmpty() )
1777     {
1778       bool unique = true;
1779       if ( mCheckPrimaryKeyUnicity )
1780       {
1781         unique = uniqueData( primaryKey );
1782       }
1783 
1784       if ( mUseEstimatedMetadata || unique )
1785       {
1786         mPrimaryKeyType = PktFidMap; // Map by default
1787         if ( mPrimaryKeyAttrs.size() == 1 )
1788         {
1789           QgsField fld = mAttributeFields.at( mPrimaryKeyAttrs.at( 0 ) );
1790           mPrimaryKeyType = pkType( fld );
1791         }
1792       }
1793       else
1794       {
1795         QgsMessageLog::logMessage( tr( "Primary key field '%1' for view/query not unique." ).arg( primaryKey ), tr( "PostGIS" ) );
1796       }
1797     }
1798     else
1799     {
1800       QgsMessageLog::logMessage( tr( "Keys for view/query undefined." ), tr( "PostGIS" ) );
1801     }
1802   }
1803   else
1804   {
1805     QgsMessageLog::logMessage( tr( "No key field for view/query given." ), tr( "PostGIS" ) );
1806   }
1807 }
1808 
uniqueData(const QString & quotedColNames)1809 bool QgsPostgresProvider::uniqueData( const QString &quotedColNames )
1810 {
1811   // Check to see if the given columns contain unique data
1812   QString sql = QStringLiteral( "SELECT count(distinct (%1))=count((%1)) FROM %2%3" )
1813                 .arg( quotedColNames,
1814                       mQuery,
1815                       filterWhereClause() );
1816 
1817   QgsPostgresResult unique( connectionRO()->PQexec( sql ) );
1818 
1819   if ( unique.PQresultStatus() != PGRES_TUPLES_OK )
1820   {
1821     pushError( unique.PQresultErrorMessage() );
1822     return false;
1823   }
1824   return unique.PQntuples() == 1 && unique.PQgetvalue( 0, 0 ).startsWith( 't' );
1825 }
1826 
1827 // Returns the minimum value of an attribute
minimumValue(int index) const1828 QVariant QgsPostgresProvider::minimumValue( int index ) const
1829 {
1830   try
1831   {
1832     // get the field name
1833     QgsField fld = field( index );
1834     QString sql = QStringLiteral( "SELECT min(%1) AS %1 FROM %2" )
1835                   .arg( quotedIdentifier( fld.name() ),
1836                         mQuery );
1837 
1838     if ( !mSqlWhereClause.isEmpty() )
1839     {
1840       sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
1841     }
1842 
1843     sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql );
1844 
1845     QgsPostgresResult rmin( connectionRO()->PQexec( sql ) );
1846     return convertValue( fld.type(), fld.subType(), rmin.PQgetvalue( 0, 0 ), fld.typeName() );
1847   }
1848   catch ( PGFieldNotFound )
1849   {
1850     return QVariant( QString() );
1851   }
1852 }
1853 
1854 // Returns the list of unique values of an attribute
uniqueValues(int index,int limit) const1855 QSet<QVariant> QgsPostgresProvider::uniqueValues( int index, int limit ) const
1856 {
1857   QSet<QVariant> uniqueValues;
1858 
1859   try
1860   {
1861     // get the field name
1862     QgsField fld = field( index );
1863     QString sql = QStringLiteral( "SELECT DISTINCT %1 FROM %2" )
1864                   .arg( quotedIdentifier( fld.name() ),
1865                         mQuery );
1866 
1867     if ( !mSqlWhereClause.isEmpty() )
1868     {
1869       sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
1870     }
1871 
1872     sql += QStringLiteral( " ORDER BY %1" ).arg( quotedIdentifier( fld.name() ) );
1873 
1874     if ( limit >= 0 )
1875     {
1876       sql += QStringLiteral( " LIMIT %1" ).arg( limit );
1877     }
1878 
1879     sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql );
1880 
1881     QgsPostgresResult res( connectionRO()->PQexec( sql ) );
1882     if ( res.PQresultStatus() == PGRES_TUPLES_OK )
1883     {
1884       for ( int i = 0; i < res.PQntuples(); i++ )
1885         uniqueValues.insert( convertValue( fld.type(), fld.subType(), res.PQgetvalue( i, 0 ), fld.typeName() ) );
1886     }
1887   }
1888   catch ( PGFieldNotFound )
1889   {
1890   }
1891   return uniqueValues;
1892 }
1893 
uniqueStringsMatching(int index,const QString & substring,int limit,QgsFeedback * feedback) const1894 QStringList QgsPostgresProvider::uniqueStringsMatching( int index, const QString &substring, int limit, QgsFeedback *feedback ) const
1895 {
1896   QStringList results;
1897 
1898   try
1899   {
1900     // get the field name
1901     QgsField fld = field( index );
1902     QString sql = QStringLiteral( "SELECT DISTINCT %1 FROM %2 WHERE" )
1903                   .arg( quotedIdentifier( fld.name() ),
1904                         mQuery );
1905 
1906     if ( !mSqlWhereClause.isEmpty() )
1907     {
1908       sql += QStringLiteral( " ( %1 ) AND " ).arg( mSqlWhereClause );
1909     }
1910 
1911     sql += QStringLiteral( " %1::text ILIKE '%%2%'" ).arg( quotedIdentifier( fld.name() ), substring );
1912 
1913 
1914     sql += QStringLiteral( " ORDER BY %1" ).arg( quotedIdentifier( fld.name() ) );
1915 
1916     if ( limit >= 0 )
1917     {
1918       sql += QStringLiteral( " LIMIT %1" ).arg( limit );
1919     }
1920 
1921     sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql );
1922 
1923     QgsPostgresResult res( connectionRO()->PQexec( sql ) );
1924     if ( res.PQresultStatus() == PGRES_TUPLES_OK )
1925     {
1926       for ( int i = 0; i < res.PQntuples(); i++ )
1927       {
1928         results << ( convertValue( fld.type(), fld.subType(), res.PQgetvalue( i, 0 ), fld.typeName() ) ).toString();
1929         if ( feedback && feedback->isCanceled() )
1930           break;
1931       }
1932     }
1933   }
1934   catch ( PGFieldNotFound )
1935   {
1936   }
1937   return results;
1938 }
1939 
enumValues(int index,QStringList & enumList) const1940 void QgsPostgresProvider::enumValues( int index, QStringList &enumList ) const
1941 {
1942 
1943   if ( index < 0 || index >= mAttributeFields.count() )
1944     return;
1945 
1946   if ( ! mShared->fieldSupportsEnumValuesIsSet( index ) )
1947   {
1948     mShared->setFieldSupportsEnumValues( index, true );
1949   }
1950   else if ( ! mShared->fieldSupportsEnumValues( index ) )
1951   {
1952     return;
1953   }
1954 
1955   //find out type of index
1956   const QString fieldName = mAttributeFields.at( index ).name();
1957   QString typeName = mAttributeFields.at( index ).typeName();
1958 
1959   // Remove schema extension from typeName
1960   typeName.remove( QRegularExpression( "^([^.]+\\.)+" ) );
1961 
1962   //is type an enum?
1963   const QString typeSql = QStringLiteral( "SELECT typtype FROM pg_type WHERE typname=%1" ).arg( quotedValue( typeName ) );
1964   QgsPostgresResult typeRes( connectionRO()->PQexec( typeSql ) );
1965   if ( typeRes.PQresultStatus() != PGRES_TUPLES_OK || typeRes.PQntuples() < 1 )
1966   {
1967     mShared->setFieldSupportsEnumValues( index, false );
1968     return;
1969   }
1970 
1971   const QString typtype = typeRes.PQgetvalue( 0, 0 );
1972   if ( typtype.compare( QLatin1String( "e" ), Qt::CaseInsensitive ) == 0 )
1973   {
1974     //try to read enum_range of attribute
1975     if ( !parseEnumRange( enumList, fieldName ) )
1976     {
1977       mShared->setFieldSupportsEnumValues( index, false );
1978     }
1979   }
1980   else
1981   {
1982     //is there a domain check constraint for the attribute?
1983     if ( !parseDomainCheckConstraint( enumList, fieldName ) )
1984     {
1985       mShared->setFieldSupportsEnumValues( index, false );
1986     }
1987   }
1988 }
1989 
parseEnumRange(QStringList & enumValues,const QString & attributeName) const1990 bool QgsPostgresProvider::parseEnumRange( QStringList &enumValues, const QString &attributeName ) const
1991 {
1992   enumValues.clear();
1993 
1994   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)" )
1995                          .arg( quotedValue( mQuery ),
1996                                quotedValue( attributeName ) );
1997   QgsPostgresResult enumRangeRes( connectionRO()->PQexec( enumRangeSql ) );
1998   if ( enumRangeRes.PQresultStatus() != PGRES_TUPLES_OK )
1999     return false;
2000 
2001   for ( int i = 0; i < enumRangeRes.PQntuples(); i++ )
2002   {
2003     enumValues << enumRangeRes.PQgetvalue( i, 0 );
2004   }
2005 
2006   return true;
2007 }
2008 
parseDomainCheckConstraint(QStringList & enumValues,const QString & attributeName) const2009 bool QgsPostgresProvider::parseDomainCheckConstraint( QStringList &enumValues, const QString &attributeName ) const
2010 {
2011   enumValues.clear();
2012 
2013   //is it a domain type with a check constraint?
2014   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 ) );
2015   QgsPostgresResult domainResult( connectionRO()->PQexec( domainSql ) );
2016   if ( domainResult.PQresultStatus() == PGRES_TUPLES_OK && domainResult.PQntuples() > 0 && !domainResult.PQgetvalue( 0, 0 ).isNull() )
2017   {
2018     QString domainCheckDefinitionSql;
2019     if ( connectionRO()->pgVersion() < 120000 )
2020     {
2021       domainCheckDefinitionSql = QStringLiteral( ""
2022                                  "SELECT consrc FROM pg_constraint "
2023                                  "  WHERE contypid =("
2024                                  "    SELECT oid FROM pg_type "
2025                                  "      WHERE typname = %1 "
2026                                  "      AND typnamespace =("
2027                                  "        SELECT oid FROM pg_namespace WHERE nspname = %2"
2028                                  "      )"
2029                                  "    )" )
2030                                  .arg( quotedValue( domainResult.PQgetvalue( 0, 0 ) ) )
2031                                  .arg( quotedValue( domainResult.PQgetvalue( 0, 1 ) ) );
2032 
2033     }
2034     else
2035     {
2036       domainCheckDefinitionSql = QStringLiteral( ""
2037                                  "SELECT pg_catalog.pg_get_constraintdef( ( "
2038                                  "  SELECT oid FROM pg_constraint WHERE contypid = ( "
2039                                  "    SELECT oid FROM pg_type "
2040                                  "      WHERE typname = %1 "
2041                                  "      AND typnamespace =("
2042                                  "        SELECT oid FROM pg_namespace WHERE nspname = %2"
2043                                  "      )"
2044                                  "    )"
2045                                  "  ), true );" )
2046                                  .arg( quotedValue( domainResult.PQgetvalue( 0, 0 ) ) )
2047                                  .arg( quotedValue( domainResult.PQgetvalue( 0, 1 ) ) );
2048 
2049     }
2050 
2051     QgsPostgresResult domainCheckRes( connectionRO()->PQexec( domainCheckDefinitionSql ) );
2052     if ( domainCheckRes.PQresultStatus() == PGRES_TUPLES_OK && domainCheckRes.PQntuples() > 0 )
2053     {
2054       QString checkDefinition = domainCheckRes.PQgetvalue( 0, 0 );
2055 
2056       //we assume that the constraint is of the following form:
2057       //(VALUE = ANY (ARRAY['a'::text, 'b'::text, 'c'::text, 'd'::text]))
2058       //normally, PostgreSQL creates that if the constraint has been specified as 'VALUE in ('a', 'b', 'c', 'd')
2059       const thread_local QRegularExpression definitionRegExp( "VALUE\\s*=\\s*ANY\\s*\\(\\s*ARRAY\\s*\\[" );
2060       int anyPos = checkDefinition.indexOf( definitionRegExp );
2061       int arrayPosition = checkDefinition.lastIndexOf( QLatin1String( "ARRAY[" ) );
2062       int closingBracketPos = checkDefinition.indexOf( ']', arrayPosition + 6 );
2063 
2064       if ( anyPos == -1 || anyPos >= arrayPosition )
2065       {
2066         return false; //constraint has not the required format
2067       }
2068 
2069       if ( arrayPosition != -1 )
2070       {
2071         QString valueList = checkDefinition.mid( arrayPosition + 6, closingBracketPos );
2072 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
2073         const QStringList commaSeparation = valueList.split( ',', QString::SkipEmptyParts );
2074 #else
2075         const QStringList commaSeparation = valueList.split( ',', Qt::SkipEmptyParts );
2076 #endif
2077         QStringList::const_iterator cIt = commaSeparation.constBegin();
2078         for ( ; cIt != commaSeparation.constEnd(); ++cIt )
2079         {
2080           //get string between ''
2081           int beginQuotePos = cIt->indexOf( '\'' );
2082           int endQuotePos = cIt->lastIndexOf( '\'' );
2083           if ( beginQuotePos != -1 && ( endQuotePos - beginQuotePos ) > 1 )
2084           {
2085             enumValues << cIt->mid( beginQuotePos + 1, endQuotePos - beginQuotePos - 1 );
2086           }
2087         }
2088       }
2089       return true;
2090     }
2091   }
2092   return false;
2093 }
2094 
2095 // Returns the maximum value of an attribute
maximumValue(int index) const2096 QVariant QgsPostgresProvider::maximumValue( int index ) const
2097 {
2098   try
2099   {
2100     // get the field name
2101     QgsField fld = field( index );
2102     QString sql = QStringLiteral( "SELECT max(%1) AS %1 FROM %2" )
2103                   .arg( quotedIdentifier( fld.name() ),
2104                         mQuery );
2105 
2106     if ( !mSqlWhereClause.isEmpty() )
2107     {
2108       sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
2109     }
2110 
2111     sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql );
2112 
2113     QgsPostgresResult rmax( connectionRO()->PQexec( sql ) );
2114 
2115     return convertValue( fld.type(), fld.subType(), rmax.PQgetvalue( 0, 0 ), fld.typeName() );
2116   }
2117   catch ( PGFieldNotFound )
2118   {
2119     return QVariant( QString() );
2120   }
2121 }
2122 
2123 
isValid() const2124 bool QgsPostgresProvider::isValid() const
2125 {
2126   return mValid;
2127 }
2128 
defaultValueClause(int fieldId) const2129 QString QgsPostgresProvider::defaultValueClause( int fieldId ) const
2130 {
2131   QString defVal = mDefaultValues.value( fieldId, QString() );
2132 
2133   // with generated columns (PostgreSQL 12+), the provider will ALWAYS evaluate the default values.
2134   // The only acceptable value for such columns on INSERT or UPDATE clauses is the keyword "DEFAULT".
2135   // Here, we return the expression used to generate the field value, so the
2136   // user can see what is happening when inserting a new feature.
2137   // On inserting a new feature or updating a generated field, this is
2138   // omitted from the generated queries.
2139   // See https://www.postgresql.org/docs/12/ddl-generated-columns.html
2140   if ( mGeneratedValues.contains( fieldId ) )
2141   {
2142     return defVal;
2143   }
2144 
2145   if ( !providerProperty( EvaluateDefaultValues, false ).toBool() && !defVal.isEmpty() )
2146   {
2147     return defVal;
2148   }
2149 
2150   return QString();
2151 }
2152 
defaultValue(int fieldId) const2153 QVariant QgsPostgresProvider::defaultValue( int fieldId ) const
2154 {
2155   QString defVal = mDefaultValues.value( fieldId, QString() );
2156 
2157   if ( providerProperty( EvaluateDefaultValues, false ).toBool() && !defVal.isEmpty() )
2158   {
2159     QgsField fld = field( fieldId );
2160 
2161     QgsPostgresResult res( connectionRO()->PQexec( QStringLiteral( "SELECT %1" ).arg( defVal ) ) );
2162 
2163     if ( res.result() )
2164     {
2165       return convertValue( fld.type(), fld.subType(), res.PQgetvalue( 0, 0 ), fld.typeName() );
2166     }
2167     else
2168     {
2169       pushError( tr( "Could not execute query" ) );
2170       return QVariant();
2171     }
2172   }
2173 
2174   return QVariant();
2175 }
2176 
skipConstraintCheck(int fieldIndex,QgsFieldConstraints::Constraint,const QVariant & value) const2177 bool QgsPostgresProvider::skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint, const QVariant &value ) const
2178 {
2179   if ( providerProperty( EvaluateDefaultValues, false ).toBool() )
2180   {
2181     return !mDefaultValues.value( fieldIndex ).isEmpty();
2182   }
2183   else
2184   {
2185     // stricter check - if we are evaluating default values only on commit then we can only bypass the check
2186     // if the attribute values matches the original default clause
2187     return mDefaultValues.contains( fieldIndex ) && mDefaultValues.value( fieldIndex ) == value.toString() && !value.isNull();
2188   }
2189 }
2190 
paramValue(const QString & fieldValue,const QString & defaultValue) const2191 QString QgsPostgresProvider::paramValue( const QString &fieldValue, const QString &defaultValue ) const
2192 {
2193   if ( fieldValue.isNull() )
2194     return QString();
2195 
2196   if ( fieldValue == defaultValue && !defaultValue.isNull() )
2197   {
2198     QgsPostgresResult result( connectionRO()->PQexec( QStringLiteral( "SELECT %1" ).arg( defaultValue ) ) );
2199     if ( result.PQresultStatus() != PGRES_TUPLES_OK )
2200       throw PGException( result );
2201 
2202     return result.PQgetvalue( 0, 0 );
2203   }
2204 
2205   return fieldValue;
2206 }
2207 
2208 
2209 /* private */
getTopoLayerInfo()2210 bool QgsPostgresProvider::getTopoLayerInfo()
2211 {
2212   QString sql = QString( "SELECT t.name, l.layer_id "
2213                          "FROM topology.layer l, topology.topology t "
2214                          "WHERE l.topology_id = t.id AND l.schema_name=%1 "
2215                          "AND l.table_name=%2 AND l.feature_column=%3" )
2216                 .arg( quotedValue( mSchemaName ),
2217                       quotedValue( mTableName ),
2218                       quotedValue( mGeometryColumn ) );
2219   QgsPostgresResult result( connectionRO()->PQexec( sql ) );
2220   if ( result.PQresultStatus() != PGRES_TUPLES_OK )
2221   {
2222     throw PGException( result ); // we should probably not do this
2223   }
2224   if ( result.PQntuples() < 1 )
2225   {
2226     QgsMessageLog::logMessage( tr( "Could not find topology of layer %1.%2.%3" )
2227                                .arg( quotedValue( mSchemaName ),
2228                                      quotedValue( mTableName ),
2229                                      quotedValue( mGeometryColumn ) ),
2230                                tr( "PostGIS" ) );
2231     return false;
2232   }
2233   mTopoLayerInfo.topologyName = result.PQgetvalue( 0, 0 );
2234   mTopoLayerInfo.layerId = result.PQgetvalue( 0, 1 ).toLong();
2235   return true;
2236 }
2237 
2238 /* private */
dropOrphanedTopoGeoms()2239 void QgsPostgresProvider::dropOrphanedTopoGeoms()
2240 {
2241   QString sql = QString( "DELETE FROM %1.relation WHERE layer_id = %2 AND "
2242                          "topogeo_id NOT IN ( SELECT id(%3) FROM %4.%5 )" )
2243                 .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) )
2244                 .arg( mTopoLayerInfo.layerId )
2245                 .arg( quotedIdentifier( mGeometryColumn ),
2246                       quotedIdentifier( mSchemaName ),
2247                       quotedIdentifier( mTableName ) )
2248                 ;
2249 
2250   QgsDebugMsgLevel( "TopoGeom orphans cleanup query: " + sql, 2 );
2251 
2252   connectionRW()->PQexecNR( sql );
2253 }
2254 
geomParam(int offset) const2255 QString QgsPostgresProvider::geomParam( int offset ) const
2256 {
2257   QString geometry;
2258 
2259   bool forceMulti = false;
2260 
2261   if ( mSpatialColType != SctTopoGeometry )
2262   {
2263     forceMulti = QgsWkbTypes::isMultiType( wkbType() );
2264   }
2265 
2266   if ( mSpatialColType == SctTopoGeometry )
2267   {
2268     geometry += QLatin1String( "toTopoGeom(" );
2269   }
2270 
2271   if ( forceMulti )
2272   {
2273     geometry += connectionRO()->majorVersion() < 2 ? "multi(" : "st_multi(";
2274   }
2275 
2276   geometry += QStringLiteral( "%1($%2%3,%4)" )
2277               .arg( connectionRO()->majorVersion() < 2 ? "geomfromwkb" : "st_geomfromwkb" )
2278               .arg( offset )
2279               .arg( connectionRO()->useWkbHex() ? "" : "::bytea",
2280                     mRequestedSrid.isEmpty() ? mDetectedSrid : mRequestedSrid );
2281 
2282   if ( forceMulti )
2283   {
2284     geometry += ')';
2285   }
2286 
2287   if ( mSpatialColType == SctTopoGeometry )
2288   {
2289     geometry += QStringLiteral( ",%1,%2)" )
2290                 .arg( quotedValue( mTopoLayerInfo.topologyName ) )
2291                 .arg( mTopoLayerInfo.layerId );
2292   }
2293 
2294   return geometry;
2295 }
2296 
addFeatures(QgsFeatureList & flist,Flags flags)2297 bool QgsPostgresProvider::addFeatures( QgsFeatureList &flist, Flags flags )
2298 {
2299   if ( flist.isEmpty() )
2300     return true;
2301 
2302   if ( mIsQuery )
2303     return false;
2304 
2305   QgsPostgresConn *conn = connectionRW();
2306   if ( !conn )
2307   {
2308     return false;
2309   }
2310   conn->lock();
2311 
2312   bool returnvalue = true;
2313 
2314   try
2315   {
2316     conn->begin();
2317 
2318     // Prepare the INSERT statement
2319     QString insert = QStringLiteral( "INSERT INTO %1(" ).arg( mQuery );
2320     QString values;
2321     QString delim;
2322     int offset = 1;
2323 
2324     QStringList defaultValues;
2325     QList<int> fieldId;
2326 
2327     if ( !mGeometryColumn.isNull() )
2328     {
2329       insert += quotedIdentifier( mGeometryColumn );
2330 
2331       values += geomParam( offset++ );
2332 
2333       delim = ',';
2334     }
2335 
2336     // Optimization: if we have a single primary key column whose default value
2337     // is a sequence, and that none of the features have a value set for that
2338     // column, then we can completely omit inserting it.
2339     bool skipSinglePKField = false;
2340     bool overrideIdentity = false;
2341 
2342     if ( ( mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktUint64 ) )
2343     {
2344       if ( mPrimaryKeyAttrs.size() == 1 &&
2345            defaultValueClause( mPrimaryKeyAttrs[0] ).startsWith( "nextval(" ) )
2346       {
2347         bool foundNonEmptyPK = false;
2348         int idx = mPrimaryKeyAttrs[0];
2349         QString defaultValue = defaultValueClause( idx );
2350         for ( int i = 0; i < flist.size(); i++ )
2351         {
2352           QgsAttributes attrs2 = flist[i].attributes();
2353           QVariant v2 = attrs2.value( idx, QVariant( QVariant::Int ) );
2354           // a PK field with a sequence val is auto populate by QGIS with this default
2355           // we are only interested in non default values
2356           if ( !v2.isNull() && v2.toString() != defaultValue )
2357           {
2358             foundNonEmptyPK = true;
2359             break;
2360           }
2361         }
2362         skipSinglePKField = !foundNonEmptyPK;
2363       }
2364 
2365       if ( !skipSinglePKField )
2366       {
2367         for ( int idx : mPrimaryKeyAttrs )
2368         {
2369           if ( mIdentityFields[idx] == 'a' )
2370             overrideIdentity = true;
2371           insert += delim + quotedIdentifier( field( idx ).name() );
2372           values += delim + QStringLiteral( "$%1" ).arg( defaultValues.size() + offset );
2373           delim = ',';
2374           fieldId << idx;
2375           defaultValues << defaultValueClause( idx );
2376         }
2377       }
2378     }
2379 
2380     QgsAttributes attributevec = flist[0].attributes();
2381 
2382     // look for unique attribute values to place in statement instead of passing as parameter
2383     // e.g. for defaults
2384     for ( int idx = 0; idx < attributevec.count(); ++idx )
2385     {
2386       QVariant v = attributevec.value( idx, QVariant( QVariant::Int ) ); // default to NULL for missing attributes
2387       if ( skipSinglePKField && idx == mPrimaryKeyAttrs[0] )
2388         continue;
2389       if ( fieldId.contains( idx ) )
2390         continue;
2391 
2392       if ( idx >= mAttributeFields.count() )
2393         continue;
2394 
2395       QString fieldname = mAttributeFields.at( idx ).name();
2396 
2397       if ( !mGeneratedValues.value( idx, QString() ).isEmpty() )
2398       {
2399         QgsDebugMsg( QStringLiteral( "Skipping field %1 (idx %2) which is GENERATED." ).arg( fieldname, QString::number( idx ) ) );
2400         continue;
2401       }
2402 
2403       QString fieldTypeName = mAttributeFields.at( idx ).typeName();
2404 
2405       QgsDebugMsgLevel( "Checking field against: " + fieldname, 2 );
2406 
2407       if ( fieldname.isEmpty() || fieldname == mGeometryColumn )
2408         continue;
2409 
2410       int i;
2411       for ( i = 1; i < flist.size(); i++ )
2412       {
2413         QgsAttributes attrs2 = flist[i].attributes();
2414         QVariant v2 = attrs2.value( idx, QVariant( QVariant::Int ) ); // default to NULL for missing attributes
2415 
2416         if ( v2 != v )
2417           break;
2418       }
2419 
2420       insert += delim + quotedIdentifier( fieldname );
2421 
2422       if ( mIdentityFields[idx] == 'a' )
2423         overrideIdentity = true;
2424 
2425       QString defVal = defaultValueClause( idx );
2426 
2427       if ( i == flist.size() )
2428       {
2429         if ( qgsVariantEqual( v, defVal ) )
2430         {
2431           if ( defVal.isNull() )
2432           {
2433             values += delim + "NULL";
2434           }
2435           else
2436           {
2437             values += delim + defVal;
2438           }
2439         }
2440         else if ( fieldTypeName == QLatin1String( "geometry" ) )
2441         {
2442           values += QStringLiteral( "%1%2(%3)" )
2443                     .arg( delim,
2444                           connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt",
2445                           quotedValue( v.toString() ) );
2446         }
2447         else if ( fieldTypeName == QLatin1String( "geography" ) )
2448         {
2449           values += QStringLiteral( "%1st_geographyfromewkt(%2)" )
2450                     .arg( delim,
2451                           quotedValue( v.toString() ) );
2452         }
2453         else if ( fieldTypeName == QLatin1String( "jsonb" ) )
2454         {
2455           values += delim + quotedJsonValue( v ) + QStringLiteral( "::jsonb" );
2456         }
2457         else if ( fieldTypeName == QLatin1String( "json" ) )
2458         {
2459           values += delim + quotedJsonValue( v ) + QStringLiteral( "::json" );
2460         }
2461         else if ( fieldTypeName == QLatin1String( "bytea" ) )
2462         {
2463           values += delim + quotedByteaValue( v );
2464         }
2465         //TODO: convert arrays and hstore to native types
2466         else
2467         {
2468           values += delim + quotedValue( v );
2469         }
2470       }
2471       else
2472       {
2473         // value is not unique => add parameter
2474         if ( fieldTypeName == QLatin1String( "geometry" ) )
2475         {
2476           values += QStringLiteral( "%1%2($%3)" )
2477                     .arg( delim,
2478                           connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" )
2479                     .arg( defaultValues.size() + offset );
2480         }
2481         else if ( fieldTypeName == QLatin1String( "geography" ) )
2482         {
2483           values += QStringLiteral( "%1st_geographyfromewkt($%2)" )
2484                     .arg( delim )
2485                     .arg( defaultValues.size() + offset );
2486         }
2487         else
2488         {
2489           values += QStringLiteral( "%1$%2" )
2490                     .arg( delim )
2491                     .arg( defaultValues.size() + offset );
2492         }
2493         defaultValues.append( defVal );
2494         fieldId.append( idx );
2495       }
2496 
2497       delim = ',';
2498     }
2499 
2500     insert += QStringLiteral( ") %1VALUES (%2)" ).arg( overrideIdentity ? "OVERRIDING SYSTEM VALUE " : "" ).arg( values );
2501 
2502     if ( !( flags & QgsFeatureSink::FastInsert ) )
2503     {
2504       if ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktUint64 )
2505       {
2506         insert += QLatin1String( " RETURNING " );
2507 
2508         QString delim;
2509         const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
2510         for ( int idx : constMPrimaryKeyAttrs )
2511         {
2512           insert += delim + quotedIdentifier( mAttributeFields.at( idx ).name() );
2513           delim = ',';
2514         }
2515       }
2516     }
2517 
2518     QgsDebugMsgLevel( QStringLiteral( "prepare addfeatures: %1" ).arg( insert ), 2 );
2519     QgsPostgresResult stmt( conn->PQprepare( QStringLiteral( "addfeatures" ), insert, fieldId.size() + offset - 1, nullptr ) );
2520 
2521     if ( stmt.PQresultStatus() != PGRES_COMMAND_OK )
2522       throw PGException( stmt );
2523 
2524     for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); ++features )
2525     {
2526       QgsAttributes attrs = features->attributes();
2527 
2528       QStringList params;
2529       if ( !mGeometryColumn.isNull() )
2530       {
2531         appendGeomParam( features->geometry(), params );
2532       }
2533 
2534       params.reserve( fieldId.size() );
2535       for ( int i = 0; i < fieldId.size(); i++ )
2536       {
2537         int attrIdx = fieldId[i];
2538         QVariant value = attrIdx < attrs.length() ? attrs.at( attrIdx ) : QVariant( QVariant::Int );
2539 
2540         QString v;
2541         if ( value.isNull() )
2542         {
2543           QgsField fld = field( attrIdx );
2544           v = paramValue( defaultValues[ i ], defaultValues[ i ] );
2545           features->setAttribute( attrIdx, convertValue( fld.type(), fld.subType(), v, fld.typeName() ) );
2546         }
2547         else
2548         {
2549           // the conversion functions expects the list as a string, so convert it
2550           if ( value.type() == QVariant::StringList )
2551           {
2552             QStringList list_vals = value.toStringList();
2553             // all strings need to be double quoted to allow special postgres
2554             // array characters such as {, or whitespace in the string
2555             // but we need to escape all double quotes and backslashes
2556             list_vals.replaceInStrings( "\\", "\\\\" );
2557             list_vals.replaceInStrings( "\"", "\\\"" );
2558             v = QStringLiteral( "{\"" ) + value.toStringList().join( QLatin1String( "\",\"" ) ) + QStringLiteral( "\"}" );
2559           }
2560           else if ( value.type() == QVariant::List )
2561           {
2562             v = "{" + value.toStringList().join( "," ) + "}";
2563           }
2564           else
2565           {
2566             v = paramValue( value.toString(), defaultValues[ i ] );
2567           }
2568 
2569           if ( v != value.toString() )
2570           {
2571             QgsField fld = field( attrIdx );
2572             features->setAttribute( attrIdx, convertValue( fld.type(), fld.subType(), v, fld.typeName() ) );
2573           }
2574         }
2575 
2576         params << v;
2577       }
2578 
2579       QgsPostgresResult result( conn->PQexecPrepared( QStringLiteral( "addfeatures" ), params ) );
2580 
2581       if ( !( flags & QgsFeatureSink::FastInsert ) && result.PQresultStatus() == PGRES_TUPLES_OK )
2582       {
2583         for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
2584         {
2585           const int idx = mPrimaryKeyAttrs.at( i );
2586           const QgsField fld = mAttributeFields.at( idx );
2587           features->setAttribute( idx, convertValue( fld.type(), fld.subType(), result.PQgetvalue( 0, i ), fld.typeName() ) );
2588         }
2589       }
2590       else if ( result.PQresultStatus() != PGRES_COMMAND_OK )
2591         throw PGException( result );
2592 
2593       if ( !( flags & QgsFeatureSink::FastInsert ) && mPrimaryKeyType == PktOid )
2594       {
2595         features->setId( result.PQoidValue() );
2596         QgsDebugMsgLevel( QStringLiteral( "new fid=%1" ).arg( features->id() ), 4 );
2597       }
2598     }
2599 
2600     if ( !( flags & QgsFeatureSink::FastInsert ) )
2601     {
2602       // update feature ids
2603       if ( mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktUint64 )
2604       {
2605         for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); ++features )
2606         {
2607           QgsAttributes attrs = features->attributes();
2608 
2609           if ( mPrimaryKeyType == PktInt )
2610           {
2611             features->setId( PKINT2FID( STRING_TO_FID( attrs.at( mPrimaryKeyAttrs.at( 0 ) ) ) ) );
2612           }
2613           else
2614           {
2615             QVariantList primaryKeyVals;
2616 
2617             const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
2618             for ( int idx : constMPrimaryKeyAttrs )
2619             {
2620               primaryKeyVals << attrs.at( idx );
2621             }
2622 
2623             features->setId( mShared->lookupFid( primaryKeyVals ) );
2624           }
2625           QgsDebugMsgLevel( QStringLiteral( "new fid=%1" ).arg( features->id() ), 4 );
2626         }
2627       }
2628     }
2629 
2630     conn->PQexecNR( QStringLiteral( "DEALLOCATE addfeatures" ) );
2631 
2632     returnvalue &= conn->commit();
2633     if ( mTransaction )
2634       mTransaction->dirtyLastSavePoint();
2635 
2636     mShared->addFeaturesCounted( flist.size() );
2637   }
2638   catch ( PGException &e )
2639   {
2640     pushError( tr( "PostGIS error while adding features: %1" ).arg( e.errorMessage() ) );
2641     conn->rollback();
2642     conn->PQexecNR( QStringLiteral( "DEALLOCATE addfeatures" ) );
2643     returnvalue = false;
2644   }
2645 
2646   conn->unlock();
2647   return returnvalue;
2648 }
2649 
deleteFeatures(const QgsFeatureIds & ids)2650 bool QgsPostgresProvider::deleteFeatures( const QgsFeatureIds &ids )
2651 {
2652   if ( ids.isEmpty() )
2653     return true;
2654 
2655   bool returnvalue = true;
2656 
2657   if ( mIsQuery )
2658   {
2659     QgsDebugMsg( QStringLiteral( "Cannot delete features (is a query)" ) );
2660     return false;
2661   }
2662 
2663   QgsPostgresConn *conn = connectionRW();
2664   if ( !conn )
2665   {
2666     return false;
2667   }
2668   conn->lock();
2669 
2670   try
2671   {
2672     conn->begin();
2673 
2674     QgsFeatureIds chunkIds;
2675     const int countIds = ids.size();
2676     int i = 0;
2677     for ( QgsFeatureIds::const_iterator it = ids.constBegin(); it != ids.constEnd(); ++it )
2678     {
2679       // create chunks of fids to delete, the last chunk may be smaller
2680       chunkIds.insert( *it );
2681       i++;
2682       if ( chunkIds.size() < 5000 && i < countIds )
2683         continue;
2684 
2685       const QString sql = QStringLiteral( "DELETE FROM %1 WHERE %2" )
2686                           .arg( mQuery, whereClause( chunkIds ) );
2687       QgsDebugMsgLevel( "delete sql: " + sql, 2 );
2688 
2689       //send DELETE statement and do error handling
2690       QgsPostgresResult result( conn->PQexec( sql ) );
2691       if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
2692         throw PGException( result );
2693 
2694       for ( QgsFeatureIds::const_iterator chunkIt = chunkIds.constBegin(); chunkIt != chunkIds.constEnd(); ++chunkIt )
2695       {
2696         mShared->removeFid( *chunkIt );
2697       }
2698       chunkIds.clear();
2699     }
2700 
2701     returnvalue &= conn->commit();
2702     if ( mTransaction )
2703       mTransaction->dirtyLastSavePoint();
2704 
2705     if ( mSpatialColType == SctTopoGeometry )
2706     {
2707       // NOTE: in presence of multiple TopoGeometry objects
2708       //       for the same table or when deleting a Geometry
2709       //       layer _also_ having a TopoGeometry component,
2710       //       orphans would still be left.
2711       // TODO: decouple layer from table and signal table when
2712       //       records are added or removed
2713       dropOrphanedTopoGeoms();
2714     }
2715 
2716     mShared->addFeaturesCounted( -ids.size() );
2717   }
2718   catch ( PGException &e )
2719   {
2720     pushError( tr( "PostGIS error while deleting features: %1" ).arg( e.errorMessage() ) );
2721     conn->rollback();
2722     returnvalue = false;
2723   }
2724 
2725   conn->unlock();
2726   return returnvalue;
2727 }
2728 
truncate()2729 bool QgsPostgresProvider::truncate()
2730 {
2731   bool returnvalue = true;
2732 
2733   if ( mIsQuery )
2734   {
2735     QgsDebugMsg( QStringLiteral( "Cannot truncate (is a query)" ) );
2736     return false;
2737   }
2738 
2739   QgsPostgresConn *conn = connectionRW();
2740   if ( !conn )
2741   {
2742     return false;
2743   }
2744   conn->lock();
2745 
2746   try
2747   {
2748     conn->begin();
2749 
2750     QString sql = QStringLiteral( "TRUNCATE %1" ).arg( mQuery );
2751     QgsDebugMsgLevel( "truncate sql: " + sql, 2 );
2752 
2753     //send truncate statement and do error handling
2754     QgsPostgresResult result( conn->PQexec( sql ) );
2755     if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
2756       throw PGException( result );
2757 
2758     returnvalue &= conn->commit();
2759     if ( mTransaction )
2760       mTransaction->dirtyLastSavePoint();
2761 
2762     if ( returnvalue )
2763     {
2764       if ( mSpatialColType == SctTopoGeometry )
2765       {
2766         // NOTE: in presence of multiple TopoGeometry objects
2767         //       for the same table or when deleting a Geometry
2768         //       layer _also_ having a TopoGeometry component,
2769         //       orphans would still be left.
2770         // TODO: decouple layer from table and signal table when
2771         //       records are added or removed
2772         dropOrphanedTopoGeoms();
2773       }
2774       mShared->clear();
2775     }
2776   }
2777   catch ( PGException &e )
2778   {
2779     pushError( tr( "PostGIS error while truncating: %1" ).arg( e.errorMessage() ) );
2780     conn->rollback();
2781     returnvalue = false;
2782   }
2783 
2784   conn->unlock();
2785   return returnvalue;
2786 }
2787 
addAttributes(const QList<QgsField> & attributes)2788 bool QgsPostgresProvider::addAttributes( const QList<QgsField> &attributes )
2789 {
2790   bool returnvalue = true;
2791 
2792   if ( mIsQuery )
2793     return false;
2794 
2795   if ( attributes.isEmpty() )
2796     return true;
2797 
2798   QgsPostgresConn *conn = connectionRW();
2799   if ( !conn )
2800   {
2801     return false;
2802   }
2803   conn->lock();
2804 
2805   try
2806   {
2807     conn->begin();
2808 
2809     QString delim;
2810     QString sql = QStringLiteral( "ALTER TABLE %1 " ).arg( mQuery );
2811     for ( QList<QgsField>::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter )
2812     {
2813       QString type = iter->typeName();
2814       if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) )
2815       {
2816         if ( iter->length() > 0 )
2817           type = QStringLiteral( "%1(%2)" ).arg( type ).arg( iter->length() );
2818       }
2819       else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) )
2820       {
2821         if ( iter->length() > 0 && iter->precision() > 0 )
2822           type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( iter->length() ).arg( iter->precision() );
2823       }
2824       sql.append( QStringLiteral( "%1ADD COLUMN %2 %3" ).arg( delim, quotedIdentifier( iter->name() ), type ) );
2825       delim = ',';
2826     }
2827 
2828     //send sql statement and do error handling
2829     QgsPostgresResult result( conn->PQexec( sql ) );
2830     if ( result.PQresultStatus() != PGRES_COMMAND_OK )
2831       throw PGException( result );
2832 
2833     for ( QList<QgsField>::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter )
2834     {
2835       if ( !iter->comment().isEmpty() )
2836       {
2837         sql = QStringLiteral( "COMMENT ON COLUMN %1.%2 IS %3" )
2838               .arg( mQuery,
2839                     quotedIdentifier( iter->name() ),
2840                     quotedValue( iter->comment() ) );
2841         result = conn->PQexec( sql );
2842         if ( result.PQresultStatus() != PGRES_COMMAND_OK )
2843           throw PGException( result );
2844       }
2845     }
2846 
2847     returnvalue &= conn->commit();
2848     if ( mTransaction )
2849       mTransaction->dirtyLastSavePoint();
2850   }
2851   catch ( PGException &e )
2852   {
2853     pushError( tr( "PostGIS error while adding attributes: %1" ).arg( e.errorMessage() ) );
2854     conn->rollback();
2855     returnvalue = false;
2856   }
2857 
2858   loadFields();
2859   conn->unlock();
2860   return returnvalue;
2861 }
2862 
deleteAttributes(const QgsAttributeIds & ids)2863 bool QgsPostgresProvider::deleteAttributes( const QgsAttributeIds &ids )
2864 {
2865   bool returnvalue = true;
2866 
2867   if ( mIsQuery )
2868     return false;
2869 
2870   QgsPostgresConn *conn = connectionRW();
2871   if ( !conn )
2872   {
2873     return false;
2874   }
2875   conn->lock();
2876 
2877   try
2878   {
2879     conn->begin();
2880 
2881     QList<int> idsList = ids.values();
2882     std::sort( idsList.begin(), idsList.end(), std::greater<int>() );
2883 
2884     for ( auto iter = idsList.constBegin(); iter != idsList.constEnd(); ++iter )
2885     {
2886       int index = *iter;
2887       if ( index < 0 || index >= mAttributeFields.count() )
2888         continue;
2889 
2890       QString column = mAttributeFields.at( index ).name();
2891       QString sql = QStringLiteral( "ALTER TABLE %1 DROP COLUMN %2" )
2892                     .arg( mQuery,
2893                           quotedIdentifier( column ) );
2894 
2895       //send sql statement and do error handling
2896       QgsPostgresResult result( conn->PQexec( sql ) );
2897       if ( result.PQresultStatus() != PGRES_COMMAND_OK )
2898         throw PGException( result );
2899 
2900       //delete the attribute from mAttributeFields
2901       mAttributeFields.remove( index );
2902     }
2903 
2904     returnvalue &= conn->commit();
2905     if ( mTransaction )
2906       mTransaction->dirtyLastSavePoint();
2907   }
2908   catch ( PGException &e )
2909   {
2910     pushError( tr( "PostGIS error while deleting attributes: %1" ).arg( e.errorMessage() ) );
2911     conn->rollback();
2912     returnvalue = false;
2913   }
2914 
2915   loadFields();
2916   conn->unlock();
2917   return returnvalue;
2918 }
2919 
renameAttributes(const QgsFieldNameMap & renamedAttributes)2920 bool QgsPostgresProvider::renameAttributes( const QgsFieldNameMap &renamedAttributes )
2921 {
2922   if ( mIsQuery )
2923     return false;
2924 
2925 
2926   QString sql = QStringLiteral( "BEGIN;" );
2927 
2928   QgsFieldNameMap::const_iterator renameIt = renamedAttributes.constBegin();
2929   bool returnvalue = true;
2930   for ( ; renameIt != renamedAttributes.constEnd(); ++renameIt )
2931   {
2932     int fieldIndex = renameIt.key();
2933     if ( fieldIndex < 0 || fieldIndex >= mAttributeFields.count() )
2934     {
2935       pushError( tr( "Invalid attribute index: %1" ).arg( fieldIndex ) );
2936       return false;
2937     }
2938     if ( mAttributeFields.indexFromName( renameIt.value() ) >= 0 )
2939     {
2940       //field name already in use
2941       pushError( tr( "Error renaming field %1: name '%2' already exists" ).arg( fieldIndex ).arg( renameIt.value() ) );
2942       return false;
2943     }
2944 
2945     sql += QStringLiteral( "ALTER TABLE %1 RENAME COLUMN %2 TO %3;" )
2946            .arg( mQuery,
2947                  quotedIdentifier( mAttributeFields.at( fieldIndex ).name() ),
2948                  quotedIdentifier( renameIt.value() ) );
2949   }
2950   sql += QLatin1String( "COMMIT;" );
2951 
2952   QgsPostgresConn *conn = connectionRW();
2953   if ( !conn )
2954   {
2955     return false;
2956   }
2957   conn->lock();
2958 
2959   try
2960   {
2961     conn->begin();
2962     //send sql statement and do error handling
2963     QgsPostgresResult result( conn->PQexec( sql ) );
2964     if ( result.PQresultStatus() != PGRES_COMMAND_OK )
2965       throw PGException( result );
2966     returnvalue = conn->commit();
2967     if ( mTransaction )
2968       mTransaction->dirtyLastSavePoint();
2969   }
2970   catch ( PGException &e )
2971   {
2972     pushError( tr( "PostGIS error while renaming attributes: %1" ).arg( e.errorMessage() ) );
2973     conn->rollback();
2974     returnvalue = false;
2975   }
2976 
2977   loadFields();
2978   conn->unlock();
2979   return returnvalue;
2980 }
2981 
changeAttributeValues(const QgsChangedAttributesMap & attr_map)2982 bool QgsPostgresProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
2983 {
2984   bool returnvalue = true;
2985 
2986   if ( mIsQuery )
2987     return false;
2988 
2989   if ( attr_map.isEmpty() )
2990     return true;
2991 
2992   QgsPostgresConn *conn = connectionRW();
2993   if ( !conn )
2994     return false;
2995 
2996   conn->lock();
2997 
2998   try
2999   {
3000     conn->begin();
3001 
3002     // cycle through the features
3003     for ( QgsChangedAttributesMap::const_iterator iter = attr_map.constBegin(); iter != attr_map.constEnd(); ++iter )
3004     {
3005       QgsFeatureId fid = iter.key();
3006 
3007       // skip added features
3008       if ( FID_IS_NEW( fid ) )
3009         continue;
3010 
3011       const QgsAttributeMap &attrs = iter.value();
3012       if ( attrs.isEmpty() )
3013         continue;
3014 
3015       QString sql = QStringLiteral( "UPDATE %1 SET " ).arg( mQuery );
3016 
3017       bool pkChanged = false;
3018 
3019       // cycle through the changed attributes of the feature
3020       QString delim;
3021       int numChangedFields = 0;
3022       for ( QgsAttributeMap::const_iterator siter = attrs.constBegin(); siter != attrs.constEnd(); ++siter )
3023       {
3024         try
3025         {
3026           QgsField fld = field( siter.key() );
3027 
3028           pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() );
3029 
3030           if ( mGeneratedValues.contains( siter.key() ) )
3031           {
3032             QgsLogger::warning( tr( "Changing the value of GENERATED field %1 is not allowed." ).arg( fld.name() ) );
3033             continue;
3034           }
3035 
3036           numChangedFields++;
3037 
3038           sql += delim + QStringLiteral( "%1=" ).arg( quotedIdentifier( fld.name() ) );
3039           delim = ',';
3040 
3041           QString defVal = defaultValueClause( siter.key() );
3042           if ( qgsVariantEqual( *siter, defVal ) )
3043           {
3044             sql += defVal.isNull() ? "NULL" : defVal;
3045           }
3046           else if ( fld.typeName() == QLatin1String( "geometry" ) )
3047           {
3048             sql += QStringLiteral( "%1(%2)" )
3049                    .arg( connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt",
3050                          quotedValue( siter->toString() ) );
3051           }
3052           else if ( fld.typeName() == QLatin1String( "geography" ) )
3053           {
3054             sql += QStringLiteral( "st_geographyfromewkt(%1)" )
3055                    .arg( quotedValue( siter->toString() ) );
3056           }
3057           else if ( fld.typeName() == QLatin1String( "jsonb" ) )
3058           {
3059             sql += QStringLiteral( "%1::jsonb" )
3060                    .arg( quotedJsonValue( siter.value() ) );
3061           }
3062           else if ( fld.typeName() == QLatin1String( "json" ) )
3063           {
3064             sql += QStringLiteral( "%1::json" )
3065                    .arg( quotedJsonValue( siter.value() ) );
3066           }
3067           else if ( fld.typeName() == QLatin1String( "bytea" ) )
3068           {
3069             sql += quotedByteaValue( siter.value() );
3070           }
3071           else
3072           {
3073             sql += quotedValue( *siter );
3074           }
3075         }
3076         catch ( PGFieldNotFound )
3077         {
3078           // Field was missing - shouldn't happen
3079         }
3080       }
3081 
3082       sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) );
3083 
3084       // Don't try to UPDATE an empty set of values (might happen if the table only has GENERATED fields,
3085       // or if the user only changed GENERATED fields in the form/attribute table.
3086       if ( numChangedFields > 0 )
3087       {
3088         QgsPostgresResult result( conn->PQexec( sql ) );
3089         if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3090           throw PGException( result );
3091       }
3092       else // let the user know that no field was actually changed
3093       {
3094         QgsLogger::warning( tr( "No fields were updated on the database." ) );
3095       }
3096 
3097       // update feature id map if key was changed
3098       // PktInt64 also uses a fid map even if it is a stand alone field.
3099       if ( pkChanged && ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt64 ) )
3100       {
3101         QVariantList k = mShared->removeFid( fid );
3102 
3103         int keyCount = std::min( mPrimaryKeyAttrs.size(), k.size() );
3104 
3105         for ( int i = 0; i < keyCount; i++ )
3106         {
3107           int idx = mPrimaryKeyAttrs.at( i );
3108           if ( !attrs.contains( idx ) )
3109             continue;
3110 
3111           k[i] = attrs[ idx ];
3112         }
3113 
3114         mShared->insertFid( fid, k );
3115       }
3116     }
3117 
3118     returnvalue &= conn->commit();
3119     if ( mTransaction )
3120       mTransaction->dirtyLastSavePoint();
3121   }
3122   catch ( PGException &e )
3123   {
3124     pushError( tr( "PostGIS error while changing attributes: %1" ).arg( e.errorMessage() ) );
3125     conn->rollback();
3126     returnvalue = false;
3127   }
3128 
3129   conn->unlock();
3130   return returnvalue;
3131 }
3132 
appendGeomParam(const QgsGeometry & geom,QStringList & params) const3133 void QgsPostgresProvider::appendGeomParam( const QgsGeometry &geom, QStringList &params ) const
3134 {
3135   if ( geom.isNull() )
3136   {
3137     params << QString();
3138     return;
3139   }
3140 
3141   QString param;
3142 
3143   QgsGeometry convertedGeom( convertToProviderType( geom ) );
3144   QByteArray wkb( !convertedGeom.isNull() ? convertedGeom.asWkb() : geom.asWkb() );
3145   const unsigned char *buf = reinterpret_cast< const unsigned char * >( wkb.constData() );
3146   int wkbSize = wkb.length();
3147 
3148   for ( int i = 0; i < wkbSize; ++i )
3149   {
3150     if ( connectionRO()->useWkbHex() )
3151       param += QStringLiteral( "%1" ).arg( ( int ) buf[i], 2, 16, QChar( '0' ) );
3152     else
3153       param += QStringLiteral( "\\%1" ).arg( ( int ) buf[i], 3, 8, QChar( '0' ) );
3154   }
3155   params << param;
3156 }
3157 
changeGeometryValues(const QgsGeometryMap & geometry_map)3158 bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
3159 {
3160 
3161   if ( mIsQuery || mGeometryColumn.isNull() )
3162     return false;
3163 
3164   QgsPostgresConn *conn = connectionRW();
3165   if ( !conn )
3166   {
3167     return false;
3168   }
3169   conn->lock();
3170 
3171   bool returnvalue = true;
3172 
3173   try
3174   {
3175     // Start the PostGIS transaction
3176     conn->begin();
3177 
3178     QString update;
3179     QgsPostgresResult result;
3180 
3181     if ( mSpatialColType == SctTopoGeometry )
3182     {
3183       // We will create a new TopoGeometry object with the new shape.
3184       // Later, we'll replace the old TopoGeometry with the new one,
3185       // to avoid orphans and retain higher level in an eventual
3186       // hierarchical definition
3187       update = QStringLiteral( "SELECT id(%1) FROM %2 o WHERE %3" )
3188                .arg( geomParam( 1 ),
3189                      mQuery,
3190                      pkParamWhereClause( 2 ) );
3191 
3192       QString getid = QStringLiteral( "SELECT id(%1) FROM %2 WHERE %3" )
3193                       .arg( quotedIdentifier( mGeometryColumn ),
3194                             mQuery,
3195                             pkParamWhereClause( 1 ) );
3196 
3197       QgsDebugMsgLevel( "getting old topogeometry id: " + getid, 2 );
3198 
3199       result = connectionRO()->PQprepare( QStringLiteral( "getid" ), getid, 1, nullptr );
3200       if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3201       {
3202         QgsDebugMsg( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3203                      .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( getid ) );
3204         throw PGException( result );
3205       }
3206 
3207       QString replace = QString( "UPDATE %1 SET %2="
3208                                  "( topology_id(%2),layer_id(%2),$1,type(%2) )"
3209                                  "WHERE %3" )
3210                         .arg( mQuery,
3211                               quotedIdentifier( mGeometryColumn ),
3212                               pkParamWhereClause( 2 ) );
3213       QgsDebugMsgLevel( "TopoGeom swap: " + replace, 2 );
3214       result = conn->PQprepare( QStringLiteral( "replacetopogeom" ), replace, 2, nullptr );
3215       if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3216       {
3217         QgsDebugMsg( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3218                      .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( replace ) );
3219         throw PGException( result );
3220       }
3221 
3222     }
3223     else
3224     {
3225       update = QStringLiteral( "UPDATE %1 SET %2=%3 WHERE %4" )
3226                .arg( mQuery,
3227                      quotedIdentifier( mGeometryColumn ),
3228                      geomParam( 1 ),
3229                      pkParamWhereClause( 2 ) );
3230     }
3231 
3232     QgsDebugMsgLevel( "updating: " + update, 2 );
3233 
3234     result = conn->PQprepare( QStringLiteral( "updatefeatures" ), update, 2, nullptr );
3235     if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3236     {
3237       QgsDebugMsg( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3238                    .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( update ) );
3239       throw PGException( result );
3240     }
3241 
3242     QgsDebugMsgLevel( QStringLiteral( "iterating over the map of changed geometries..." ), 2 );
3243 
3244     for ( QgsGeometryMap::const_iterator iter = geometry_map.constBegin();
3245           iter != geometry_map.constEnd();
3246           ++iter )
3247     {
3248       QgsDebugMsgLevel( "iterating over feature id " + FID_TO_STRING( iter.key() ), 2 );
3249 
3250       // Save the id of the current topogeometry
3251       long old_tg_id = -1;
3252       if ( mSpatialColType == SctTopoGeometry )
3253       {
3254         QStringList params;
3255         appendPkParams( iter.key(), params );
3256         result = connectionRO()->PQexecPrepared( QStringLiteral( "getid" ), params );
3257         if ( result.PQresultStatus() != PGRES_TUPLES_OK )
3258         {
3259           QgsDebugMsg( QStringLiteral( "Exception thrown due to PQexecPrepared of 'getid' returning != PGRES_TUPLES_OK (%1 != expected %2)" )
3260                        .arg( result.PQresultStatus() ).arg( PGRES_TUPLES_OK ) );
3261           throw PGException( result );
3262         }
3263         // TODO: watch out for NULL, handle somehow
3264         old_tg_id = result.PQgetvalue( 0, 0 ).toLong();
3265         QgsDebugMsgLevel( QStringLiteral( "Old TG id is %1" ).arg( old_tg_id ), 2 );
3266       }
3267 
3268       QStringList params;
3269       appendGeomParam( *iter, params );
3270       appendPkParams( iter.key(), params );
3271 
3272       result = conn->PQexecPrepared( QStringLiteral( "updatefeatures" ), params );
3273       if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3274         throw PGException( result );
3275 
3276       if ( mSpatialColType == SctTopoGeometry )
3277       {
3278         long new_tg_id = result.PQgetvalue( 0, 0 ).toLong(); // new topogeo_id
3279 
3280         // Replace old TopoGeom with new TopoGeom, so that
3281         // any hierarchically defined TopoGeom will still have its
3282         // definition and we'll leave no orphans
3283         QString replace = QString( "DELETE FROM %1.relation WHERE "
3284                                    "layer_id = %2 AND topogeo_id = %3" )
3285                           .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) )
3286                           .arg( mTopoLayerInfo.layerId )
3287                           .arg( old_tg_id );
3288         result = conn->PQexec( replace );
3289         if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3290         {
3291           QgsDebugMsg( QStringLiteral( "Exception thrown due to PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3292                        .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( replace ) );
3293           throw PGException( result );
3294         }
3295         // TODO: use prepared query here
3296         replace = QString( "UPDATE %1.relation SET topogeo_id = %2 "
3297                            "WHERE layer_id = %3 AND topogeo_id = %4" )
3298                   .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) )
3299                   .arg( old_tg_id )
3300                   .arg( mTopoLayerInfo.layerId )
3301                   .arg( new_tg_id );
3302         QgsDebugMsgLevel( "relation swap: " + replace, 2 );
3303         result = conn->PQexec( replace );
3304         if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3305         {
3306           QgsDebugMsg( QStringLiteral( "Exception thrown due to PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3307                        .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( replace ) );
3308           throw PGException( result );
3309         }
3310       } // if TopoGeometry
3311 
3312     } // for each feature
3313 
3314     conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeatures" ) );
3315     if ( mSpatialColType == SctTopoGeometry )
3316     {
3317       connectionRO()->PQexecNR( QStringLiteral( "DEALLOCATE getid" ) );
3318       conn->PQexecNR( QStringLiteral( "DEALLOCATE replacetopogeom" ) );
3319     }
3320 
3321     returnvalue &= conn->commit();
3322     if ( mTransaction )
3323       mTransaction->dirtyLastSavePoint();
3324   }
3325   catch ( PGException &e )
3326   {
3327     pushError( tr( "PostGIS error while changing geometry values: %1" ).arg( e.errorMessage() ) );
3328     conn->rollback();
3329     conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeatures" ) );
3330     if ( mSpatialColType == SctTopoGeometry )
3331     {
3332       connectionRO()->PQexecNR( QStringLiteral( "DEALLOCATE getid" ) );
3333       conn->PQexecNR( QStringLiteral( "DEALLOCATE replacetopogeom" ) );
3334     }
3335     returnvalue = false;
3336   }
3337 
3338   conn->unlock();
3339 
3340   QgsDebugMsgLevel( QStringLiteral( "leaving." ), 4 );
3341 
3342   return returnvalue;
3343 }
3344 
changeFeatures(const QgsChangedAttributesMap & attr_map,const QgsGeometryMap & geometry_map)3345 bool QgsPostgresProvider::changeFeatures( const QgsChangedAttributesMap &attr_map,
3346     const QgsGeometryMap &geometry_map )
3347 {
3348   Q_ASSERT( mSpatialColType != SctTopoGeometry );
3349 
3350   bool returnvalue = true;
3351 
3352   if ( mIsQuery )
3353     return false;
3354 
3355   if ( attr_map.isEmpty() )
3356     return true;
3357 
3358   QgsPostgresConn *conn = connectionRW();
3359   if ( !conn )
3360     return false;
3361 
3362   conn->lock();
3363 
3364   try
3365   {
3366     conn->begin();
3367 
3368     QgsFeatureIds ids( qgis::listToSet( attr_map.keys() ) );
3369     ids |= qgis::listToSet( geometry_map.keys() );
3370 
3371     // cycle through the features
3372     const auto constIds = ids;
3373     for ( QgsFeatureId fid : constIds )
3374     {
3375       // skip added features
3376       if ( FID_IS_NEW( fid ) )
3377         continue;
3378 
3379       const QgsAttributeMap &attrs = attr_map.value( fid );
3380       if ( attrs.isEmpty() && !geometry_map.contains( fid ) )
3381         continue;
3382 
3383       QString sql = QStringLiteral( "UPDATE %1 SET " ).arg( mQuery );
3384 
3385       bool pkChanged = false;
3386 
3387       // cycle through the changed attributes of the feature
3388       QString delim;
3389       int numChangedFields = 0;
3390 
3391       for ( QgsAttributeMap::const_iterator siter = attrs.constBegin(); siter != attrs.constEnd(); ++siter )
3392       {
3393         try
3394         {
3395           QgsField fld = field( siter.key() );
3396 
3397           pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() );
3398 
3399           if ( mGeneratedValues.contains( siter.key() ) )
3400           {
3401             QgsLogger::warning( tr( "Changing the value of GENERATED field %1 is not allowed." ).arg( fld.name() ) );
3402             continue;
3403           }
3404 
3405           numChangedFields++;
3406 
3407           sql += delim + QStringLiteral( "%1=" ).arg( quotedIdentifier( fld.name() ) );
3408           delim = ',';
3409 
3410           if ( fld.typeName() == QLatin1String( "geometry" ) )
3411           {
3412             sql += QStringLiteral( "%1(%2)" )
3413                    .arg( connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt",
3414                          quotedValue( siter->toString() ) );
3415           }
3416           else if ( fld.typeName() == QLatin1String( "geography" ) )
3417           {
3418             sql += QStringLiteral( "st_geographyfromewkt(%1)" )
3419                    .arg( quotedValue( siter->toString() ) );
3420           }
3421           else if ( fld.typeName() == QLatin1String( "jsonb" ) )
3422           {
3423             sql += QStringLiteral( "%1::jsonb" )
3424                    .arg( quotedJsonValue( siter.value() ) );
3425           }
3426           else if ( fld.typeName() == QLatin1String( "json" ) )
3427           {
3428             sql += QStringLiteral( "%1::json" )
3429                    .arg( quotedJsonValue( siter.value() ) );
3430           }
3431           else if ( fld.typeName() == QLatin1String( "bytea" ) )
3432           {
3433             sql += quotedByteaValue( siter.value() );
3434           }
3435           else
3436           {
3437             sql += quotedValue( *siter );
3438           }
3439         }
3440         catch ( PGFieldNotFound )
3441         {
3442           // Field was missing - shouldn't happen
3443         }
3444       }
3445 
3446       if ( !geometry_map.contains( fid ) )
3447       {
3448         // Don't try to UPDATE an empty set of values (might happen if the table only has GENERATED fields,
3449         // or if the user only changed GENERATED fields in the form/attribute table.
3450         if ( numChangedFields > 0 )
3451         {
3452           sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) );
3453 
3454           QgsPostgresResult result( conn->PQexec( sql ) );
3455           if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3456             throw PGException( result );
3457         }
3458         else // let the user know that nothing has actually changed
3459         {
3460           QgsLogger::warning( tr( "No fields/geometries were updated on the database." ) );
3461         }
3462       }
3463       else
3464       {
3465         sql += QStringLiteral( "%1%2=%3" ).arg( delim, quotedIdentifier( mGeometryColumn ), geomParam( 1 ) );
3466         sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) );
3467 
3468         QgsPostgresResult result( conn->PQprepare( QStringLiteral( "updatefeature" ), sql, 1, nullptr ) );
3469         if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3470         {
3471           QgsDebugMsg( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3472                        .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( sql ) );
3473           throw PGException( result );
3474         }
3475 
3476         QStringList params;
3477         const QgsGeometry &geom = geometry_map[ fid ];
3478         appendGeomParam( geom, params );
3479 
3480         result = conn->PQexecPrepared( QStringLiteral( "updatefeature" ), params );
3481         if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3482         {
3483           conn->rollback();
3484           conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeature" ) );
3485           throw PGException( result );
3486         }
3487 
3488         conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeature" ) );
3489       }
3490 
3491       // update feature id map if key was changed
3492       // PktInt64 also uses a fid map even though it is a single field.
3493       if ( pkChanged && ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt64 ) )
3494       {
3495         QVariantList k = mShared->removeFid( fid );
3496 
3497         for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ )
3498         {
3499           int idx = mPrimaryKeyAttrs.at( i );
3500           if ( !attrs.contains( idx ) )
3501             continue;
3502 
3503           k[i] = attrs[ idx ];
3504         }
3505 
3506         mShared->insertFid( fid, k );
3507       }
3508     }
3509 
3510     returnvalue &= conn->commit();
3511     if ( mTransaction )
3512       mTransaction->dirtyLastSavePoint();
3513   }
3514   catch ( PGException &e )
3515   {
3516     pushError( tr( "PostGIS error while changing attributes: %1" ).arg( e.errorMessage() ) );
3517     conn->rollback();
3518     returnvalue = false;
3519   }
3520 
3521   conn->unlock();
3522 
3523   QgsDebugMsgLevel( QStringLiteral( "leaving." ), 4 );
3524 
3525   return returnvalue;
3526 }
3527 
attributeIndexes() const3528 QgsAttributeList QgsPostgresProvider::attributeIndexes() const
3529 {
3530   QgsAttributeList lst;
3531   lst.reserve( mAttributeFields.count() );
3532   for ( int i = 0; i < mAttributeFields.count(); ++i )
3533     lst.append( i );
3534   return lst;
3535 }
3536 
capabilities() const3537 QgsVectorDataProvider::Capabilities QgsPostgresProvider::capabilities() const
3538 {
3539   return mEnabledCapabilities;
3540 }
3541 
hasSpatialIndex() const3542 QgsFeatureSource::SpatialIndexPresence QgsPostgresProvider::hasSpatialIndex() const
3543 {
3544   QgsPostgresProviderConnection conn( mUri.uri(), QVariantMap() );
3545   try
3546   {
3547     return conn.spatialIndexExists( mUri.schema(), mUri.table(), mUri.geometryColumn() ) ? SpatialIndexPresent : SpatialIndexNotPresent;
3548   }
3549   catch ( QgsProviderConnectionException & )
3550   {
3551     return SpatialIndexUnknown;
3552   }
3553 }
3554 
setSubsetString(const QString & theSQL,bool updateFeatureCount)3555 bool QgsPostgresProvider::setSubsetString( const QString &theSQL, bool updateFeatureCount )
3556 {
3557   if ( theSQL.trimmed() == mSqlWhereClause )
3558     return true;
3559 
3560   QString prevWhere = mSqlWhereClause;
3561 
3562   mSqlWhereClause = theSQL.trimmed();
3563 
3564   QString sql = QStringLiteral( "SELECT * FROM %1" ).arg( mQuery );
3565 
3566   if ( !mSqlWhereClause.isEmpty() )
3567   {
3568     sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
3569   }
3570 
3571   sql += QLatin1String( " LIMIT 0" );
3572 
3573   QgsPostgresResult res( connectionRO()->PQexec( sql ) );
3574   if ( res.PQresultStatus() != PGRES_TUPLES_OK )
3575   {
3576     pushError( res.PQresultErrorMessage() );
3577     mSqlWhereClause = prevWhere;
3578     return false;
3579   }
3580 
3581 #if 0
3582   // FIXME
3583   if ( mPrimaryKeyType == PktInt && !uniqueData( primaryKeyAttr ) )
3584   {
3585     sqlWhereClause = prevWhere;
3586     return false;
3587   }
3588 #endif
3589 
3590   // Update datasource uri too
3591   mUri.setSql( theSQL );
3592   // Update yet another copy of the uri. Why are there 3 copies of the
3593   // uri? Perhaps this needs some rationalisation.....
3594   setDataSourceUri( mUri.uri( false ) );
3595 
3596   if ( updateFeatureCount )
3597   {
3598     reloadData();
3599   }
3600   else
3601   {
3602     mLayerExtent.setMinimal();
3603     emit dataChanged();
3604   }
3605 
3606   return true;
3607 }
3608 
3609 /**
3610  * Returns the feature count
3611  */
featureCount() const3612 long long QgsPostgresProvider::featureCount() const
3613 {
3614   long long featuresCounted = mShared->featuresCounted();
3615   if ( featuresCounted >= 0 )
3616     return featuresCounted;
3617 
3618   // See: https://github.com/qgis/QGIS/issues/25285 - QGIS crashes on featureCount())
3619   if ( ! connectionRO() )
3620   {
3621     return 0;
3622   }
3623 
3624   // get total number of features
3625   QString sql;
3626 
3627   // use estimated metadata even when there is a where clause,
3628   // although we get an incorrect feature count for the subset
3629   // - but make huge dataset usable.
3630   long long num = -1;
3631   if ( !mIsQuery && mUseEstimatedMetadata )
3632   {
3633     if ( relkind() == Relkind::View && connectionRO()->pgVersion() >= 90000 )
3634     {
3635       // parse explain output to estimate feature count
3636       // we don't use pg_class reltuples because it returns 0 for view
3637       sql = QStringLiteral( "EXPLAIN (FORMAT JSON) SELECT 1 FROM %1%2" ).arg( mQuery, filterWhereClause() );
3638       QgsPostgresResult result( connectionRO()->PQexec( sql ) );
3639 
3640       const QString json = result.PQgetvalue( 0, 0 );
3641       const QVariantList explain = QgsJsonUtils::parseJson( json ).toList();
3642       const QVariantMap countPlan = !explain.isEmpty() ? explain[0].toMap().value( "Plan" ).toMap() : QVariantMap();
3643       const QVariant nbRows = countPlan.value( "Plan Rows" );
3644 
3645       if ( nbRows.isValid() )
3646         num = nbRows.toLongLong();
3647       else
3648         QgsLogger::warning( QStringLiteral( "Cannot parse JSON explain result to estimate feature count (%1) : %2" ).arg( sql, json ) );
3649     }
3650     else
3651     {
3652       sql = QStringLiteral( "SELECT reltuples::bigint FROM pg_catalog.pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) );
3653       QgsPostgresResult result( connectionRO()->PQexec( sql ) );
3654       num = result.PQgetvalue( 0, 0 ).toLongLong();
3655     }
3656   }
3657   else
3658   {
3659     sql = QStringLiteral( "SELECT count(*) FROM %1%2" ).arg( mQuery, filterWhereClause() );
3660     QgsPostgresResult result( connectionRO()->PQexec( sql ) );
3661 
3662     QgsDebugMsgLevel( "number of features as text: " + result.PQgetvalue( 0, 0 ), 2 );
3663 
3664     num = result.PQgetvalue( 0, 0 ).toLongLong();
3665   }
3666 
3667   mShared->setFeaturesCounted( num );
3668 
3669   QgsDebugMsgLevel( "number of features: " + QString::number( num ), 2 );
3670 
3671   return num;
3672 }
3673 
empty() const3674 bool QgsPostgresProvider::empty() const
3675 {
3676   QString sql = QStringLiteral( "SELECT EXISTS (SELECT * FROM %1%2 LIMIT 1)" ).arg( mQuery, filterWhereClause() );
3677   QgsPostgresResult res( connectionRO()->PQexec( sql ) );
3678   if ( res.PQresultStatus() != PGRES_TUPLES_OK )
3679   {
3680     pushError( res.PQresultErrorMessage() );
3681     return false;
3682   }
3683 
3684   return res.PQgetvalue( 0, 0 ) != QLatin1String( "t" );
3685 }
3686 
extent() const3687 QgsRectangle QgsPostgresProvider::extent() const
3688 {
3689   if ( !isValid() || mGeometryColumn.isNull() )
3690     return QgsRectangle();
3691 
3692   if ( mSpatialColType == SctGeography )
3693     return QgsRectangle( -180.0, -90.0, 180.0, 90.0 );
3694 
3695   if ( mLayerExtent.isEmpty() )
3696   {
3697     QString sql;
3698     QgsPostgresResult result;
3699     QString ext;
3700 
3701     // get the extents
3702     if ( !mIsQuery && mUseEstimatedMetadata )
3703     {
3704       // do stats exists?
3705       sql = QStringLiteral( "SELECT count(*) FROM pg_stats WHERE schemaname=%1 AND tablename=%2 AND attname=%3" )
3706             .arg( quotedValue( mSchemaName ),
3707                   quotedValue( mTableName ),
3708                   quotedValue( mGeometryColumn ) );
3709       result = connectionRO()->PQexec( sql );
3710       if ( result.PQresultStatus() == PGRES_TUPLES_OK && result.PQntuples() == 1 )
3711       {
3712         if ( result.PQgetvalue( 0, 0 ).toInt() > 0 )
3713         {
3714           sql = QStringLiteral( "SELECT reltuples::bigint FROM pg_catalog.pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) );
3715           result = connectionRO()->PQexec( sql );
3716           if ( result.PQresultStatus() == PGRES_TUPLES_OK
3717                && result.PQntuples() == 1
3718                && result.PQgetvalue( 0, 0 ).toLong() > 0 )
3719           {
3720             sql = QStringLiteral( "SELECT %1(%2,%3,%4)" )
3721                   .arg( connectionRO()->majorVersion() < 2 ? "estimated_extent" :
3722                         ( connectionRO()->majorVersion() == 2 && connectionRO()->minorVersion() < 1 ? "st_estimated_extent" : "st_estimatedextent" ),
3723                         quotedValue( mSchemaName ),
3724                         quotedValue( mTableName ),
3725                         quotedValue( mGeometryColumn ) );
3726             result = mConnectionRO->PQexec( sql );
3727             if ( result.PQresultStatus() == PGRES_TUPLES_OK && result.PQntuples() == 1 && !result.PQgetisnull( 0, 0 ) )
3728             {
3729               ext = result.PQgetvalue( 0, 0 );
3730 
3731               // fix for what might be a PostGIS bug: when the extent crosses the
3732               // dateline extent() returns -180 to 180 (which appears right), but
3733               // estimated_extent() returns eastern bound of data (>-180) and
3734               // 180 degrees.
3735               if ( !ext.startsWith( QLatin1String( "-180 " ) ) && ext.contains( QLatin1String( ",180 " ) ) )
3736               {
3737                 ext.clear();
3738               }
3739             }
3740           }
3741           else
3742           {
3743             // no features => ignore estimated extent
3744             ext.clear();
3745           }
3746         }
3747       }
3748       else
3749       {
3750         QgsDebugMsgLevel( QStringLiteral( "no column statistics for %1.%2.%3" ).arg( mSchemaName, mTableName, mGeometryColumn ), 2 );
3751       }
3752     }
3753 
3754     if ( ext.isEmpty() )
3755     {
3756       sql = QStringLiteral( "SELECT %1(%2%3) FROM %4%5" )
3757             .arg( connectionRO()->majorVersion() < 2 ? "extent" : "st_extent",
3758                   quotedIdentifier( mBoundingBoxColumn ),
3759                   mSpatialColType == SctPcPatch ? "::geometry" : "",
3760                   mQuery,
3761                   filterWhereClause() );
3762 
3763       result = connectionRO()->PQexec( sql );
3764       if ( result.PQresultStatus() != PGRES_TUPLES_OK )
3765         connectionRO()->PQexecNR( QStringLiteral( "ROLLBACK" ) );
3766       else if ( result.PQntuples() == 1 && !result.PQgetisnull( 0, 0 ) )
3767         ext = result.PQgetvalue( 0, 0 );
3768     }
3769 
3770     if ( !ext.isEmpty() )
3771     {
3772       QgsDebugMsgLevel( "Got extents using: " + sql, 2 );
3773 
3774       const QRegularExpression rx( "\\((.+) (.+),(.+) (.+)\\)" );
3775       const QRegularExpressionMatch match = rx.match( ext );
3776       if ( match.hasMatch() )
3777       {
3778         mLayerExtent.setXMinimum( match.captured( 1 ).toDouble() );
3779         mLayerExtent.setYMinimum( match.captured( 2 ).toDouble() );
3780         mLayerExtent.setXMaximum( match.captured( 3 ).toDouble() );
3781         mLayerExtent.setYMaximum( match.captured( 4 ).toDouble() );
3782       }
3783       else
3784       {
3785         QgsMessageLog::logMessage( tr( "result of extents query invalid: %1" ).arg( ext ), tr( "PostGIS" ) );
3786       }
3787     }
3788 
3789     QgsDebugMsgLevel( "Set extents to: " + mLayerExtent.toString(), 2 );
3790   }
3791 
3792   return mLayerExtent;
3793 }
3794 
updateExtents()3795 void QgsPostgresProvider::updateExtents()
3796 {
3797   mLayerExtent.setMinimal();
3798 }
3799 
getGeometryDetails()3800 bool QgsPostgresProvider::getGeometryDetails()
3801 {
3802   if ( mGeometryColumn.isNull() )
3803   {
3804     mDetectedGeomType = QgsWkbTypes::NoGeometry;
3805     mValid = true;
3806     return true;
3807   }
3808 
3809   QgsPostgresResult result;
3810   QString sql;
3811 
3812   QString schemaName = mSchemaName;
3813   QString tableName = mTableName;
3814   QString geomCol = mGeometryColumn;
3815   QString geomColType;
3816 
3817   // Trust the datasource config means that we used requested geometry type and srid
3818   // We only need to get the spatial column type
3819   if ( ( mReadFlags & QgsDataProvider::FlagTrustDataSource ) &&
3820        mRequestedGeomType != QgsWkbTypes::Unknown &&
3821        !mRequestedSrid.isEmpty() )
3822   {
3823     if ( mIsQuery )
3824     {
3825       sql = QStringLiteral(
3826               "SELECT t.typname FROM pg_type t inner join (SELECT pg_typeof(%1) typeof FROM %2 LIMIT 1) g ON oid = g.typeof"
3827             ).arg( quotedIdentifier( geomCol ), mQuery );
3828     }
3829     else
3830     {
3831       sql = QStringLiteral(
3832               "SELECT t.typname FROM pg_type t inner join (SELECT pg_typeof(%1) typeof FROM %2.%3 LIMIT 1) g ON oid = g.typeof"
3833             ).arg( quotedIdentifier( geomCol ),
3834                    quotedIdentifier( schemaName ),
3835                    quotedIdentifier( tableName ) );
3836     }
3837     QgsDebugMsgLevel( QStringLiteral( "Getting the spatial column type: %1" ).arg( sql ), 2 );
3838 
3839     result = connectionRO()->PQexec( sql );
3840     if ( PGRES_TUPLES_OK == result.PQresultStatus() )
3841     {
3842       geomColType = result.PQgetvalue( 0, 0 );
3843 
3844       // Get spatial col type
3845       if ( geomColType == QLatin1String( "geometry" ) )
3846         mSpatialColType = SctGeometry;
3847       else if ( geomColType == QLatin1String( "geography" ) )
3848         mSpatialColType = SctGeography;
3849       else if ( geomColType == QLatin1String( "topogeometry" ) )
3850         mSpatialColType = SctTopoGeometry;
3851       else if ( geomColType == QLatin1String( "pcpatch" ) )
3852         mSpatialColType = SctPcPatch;
3853       else
3854         mSpatialColType = SctNone;
3855 
3856       // Use requested geometry type and srid
3857       mDetectedGeomType = mRequestedGeomType;
3858       mDetectedSrid     = mRequestedSrid;
3859       mValid = true;
3860       return true;
3861     }
3862     else
3863     {
3864       mValid = false;
3865       return false;
3866     }
3867   }
3868 
3869   if ( mIsQuery )
3870   {
3871     sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery );
3872 
3873     QgsDebugMsgLevel( QStringLiteral( "Getting geometry column: %1" ).arg( sql ), 2 );
3874 
3875     QgsPostgresResult result( connectionRO()->PQexec( sql ) );
3876     if ( PGRES_TUPLES_OK == result.PQresultStatus() )
3877     {
3878       Oid tableoid = result.PQftable( 0 );
3879       int column = result.PQftablecol( 0 );
3880 
3881       result = connectionRO()->PQexec( sql );
3882       if ( tableoid > 0 && PGRES_TUPLES_OK == result.PQresultStatus() )
3883       {
3884         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 );
3885         result = connectionRO()->PQexec( sql );
3886 
3887         if ( PGRES_TUPLES_OK == result.PQresultStatus() && 1 == result.PQntuples() )
3888         {
3889           schemaName = result.PQgetvalue( 0, 0 );
3890           tableName = result.PQgetvalue( 0, 1 );
3891 
3892           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 );
3893           result = connectionRO()->PQexec( sql );
3894           if ( PGRES_TUPLES_OK == result.PQresultStatus() && 1 == result.PQntuples() )
3895           {
3896             geomCol = result.PQgetvalue( 0, 0 );
3897             geomColType = result.PQgetvalue( 0, 1 );
3898             if ( geomColType == QLatin1String( "geometry" ) )
3899               mSpatialColType = SctGeometry;
3900             else if ( geomColType == QLatin1String( "geography" ) )
3901               mSpatialColType = SctGeography;
3902             else if ( geomColType == QLatin1String( "topogeometry" ) )
3903               mSpatialColType = SctTopoGeometry;
3904             else if ( geomColType == QLatin1String( "pcpatch" ) )
3905               mSpatialColType = SctPcPatch;
3906             else
3907               mSpatialColType = SctNone;
3908           }
3909           else
3910           {
3911             schemaName = mSchemaName;
3912             tableName = mTableName;
3913           }
3914         }
3915       }
3916       else
3917       {
3918         schemaName.clear();
3919         tableName = mQuery;
3920       }
3921     }
3922     else
3923     {
3924       mValid = false;
3925       return false;
3926     }
3927   }
3928 
3929   QString detectedType;
3930   QString detectedSrid;
3931   if ( !schemaName.isEmpty() )
3932   {
3933     // check geometry columns
3934     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" )
3935           .arg( quotedValue( tableName ),
3936                 quotedValue( geomCol ),
3937                 quotedValue( schemaName ) );
3938 
3939     QgsDebugMsgLevel( QStringLiteral( "Getting geometry column: %1" ).arg( sql ), 2 );
3940     result = connectionRO()->PQexec( sql );
3941     QgsDebugMsgLevel( QStringLiteral( "Geometry column query returned %1 rows" ).arg( result.PQntuples() ), 2 );
3942 
3943     if ( result.PQntuples() == 1 )
3944     {
3945       detectedType = result.PQgetvalue( 0, 0 );
3946 
3947       // Do not override the SRID if set in the data source URI
3948       if ( detectedSrid.isEmpty() )
3949       {
3950         detectedSrid = result.PQgetvalue( 0, 1 );
3951       }
3952 
3953       QString dim = result.PQgetvalue( 0, 2 );
3954       if ( dim == QLatin1String( "3" ) && !detectedType.endsWith( 'M' ) )
3955         detectedType += QLatin1Char( 'Z' );
3956       else if ( dim == QLatin1String( "4" ) )
3957         detectedType += QLatin1String( "ZM" );
3958 
3959       QString ds = result.PQgetvalue( 0, 1 );
3960       if ( ds != QLatin1String( "0" ) ) detectedSrid = ds;
3961       mSpatialColType = SctGeometry;
3962     }
3963     else
3964     {
3965       connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) );
3966     }
3967 
3968     if ( detectedType.isEmpty() )
3969     {
3970       // check geography columns
3971       sql = QStringLiteral( "SELECT upper(type),srid FROM geography_columns WHERE f_table_name=%1 AND f_geography_column=%2 AND f_table_schema=%3" )
3972             .arg( quotedValue( tableName ),
3973                   quotedValue( geomCol ),
3974                   quotedValue( schemaName ) );
3975 
3976       QgsDebugMsgLevel( QStringLiteral( "Getting geography column: %1" ).arg( sql ), 2 );
3977       result = connectionRO()->PQexec( sql, false );
3978       QgsDebugMsgLevel( QStringLiteral( "Geography column query returned %1" ).arg( result.PQntuples() ), 2 );
3979 
3980       if ( result.PQntuples() == 1 )
3981       {
3982         QString dt = result.PQgetvalue( 0, 0 );
3983         if ( dt != "GEOMETRY" ) detectedType = dt;
3984         QString ds = result.PQgetvalue( 0, 1 );
3985         if ( ds != "0" ) detectedSrid = ds;
3986         mSpatialColType = SctGeography;
3987       }
3988       else
3989       {
3990         connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) );
3991       }
3992     }
3993 
3994     if ( detectedType.isEmpty() && connectionRO()->hasTopology() )
3995     {
3996       // check topology.layer
3997       sql = QString( "SELECT CASE "
3998                      "WHEN l.feature_type = 1 THEN 'MULTIPOINT' "
3999                      "WHEN l.feature_type = 2 THEN 'MULTILINESTRING' "
4000                      "WHEN l.feature_type = 3 THEN 'MULTIPOLYGON' "
4001                      "WHEN l.feature_type = 4 THEN 'GEOMETRYCOLLECTION' "
4002                      "END AS type, t.srid FROM topology.layer l, topology.topology t "
4003                      "WHERE l.topology_id = t.id AND l.schema_name=%3 "
4004                      "AND l.table_name=%1 AND l.feature_column=%2" )
4005             .arg( quotedValue( tableName ),
4006                   quotedValue( geomCol ),
4007                   quotedValue( schemaName ) );
4008 
4009       QgsDebugMsgLevel( QStringLiteral( "Getting TopoGeometry column: %1" ).arg( sql ), 2 );
4010       result = connectionRO()->PQexec( sql, false );
4011       QgsDebugMsgLevel( QStringLiteral( "TopoGeometry column query returned %1" ).arg( result.PQntuples() ), 2 );
4012 
4013       if ( result.PQntuples() == 1 )
4014       {
4015         detectedType = result.PQgetvalue( 0, 0 );
4016         detectedSrid = result.PQgetvalue( 0, 1 );
4017         mSpatialColType = SctTopoGeometry;
4018       }
4019       else
4020       {
4021         connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) );
4022       }
4023     }
4024 
4025     if ( detectedType.isEmpty() && connectionRO()->hasPointcloud() )
4026     {
4027       // check pointcloud columns
4028       sql = QStringLiteral( "SELECT 'POLYGON',srid FROM pointcloud_columns WHERE \"table\"=%1 AND \"column\"=%2 AND \"schema\"=%3" )
4029             .arg( quotedValue( tableName ),
4030                   quotedValue( geomCol ),
4031                   quotedValue( schemaName ) );
4032 
4033       QgsDebugMsgLevel( QStringLiteral( "Getting pointcloud column: %1" ).arg( sql ), 2 );
4034       result = connectionRO()->PQexec( sql, false );
4035       QgsDebugMsgLevel( QStringLiteral( "Pointcloud column query returned %1" ).arg( result.PQntuples() ), 2 );
4036 
4037       if ( result.PQntuples() == 1 )
4038       {
4039         detectedType = result.PQgetvalue( 0, 0 );
4040         detectedSrid = result.PQgetvalue( 0, 1 );
4041         mSpatialColType = SctPcPatch;
4042       }
4043       else
4044       {
4045         connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) );
4046       }
4047     }
4048 
4049     if ( mSpatialColType == SctNone )
4050     {
4051       sql = QString( "SELECT t.typname FROM "
4052                      "pg_attribute a, pg_class c, pg_namespace n, pg_type t "
4053                      "WHERE a.attrelid=c.oid AND c.relnamespace=n.oid "
4054                      "AND a.atttypid=t.oid "
4055                      "AND n.nspname=%3 AND c.relname=%1 AND a.attname=%2" )
4056             .arg( quotedValue( tableName ),
4057                   quotedValue( geomCol ),
4058                   quotedValue( schemaName ) );
4059       QgsDebugMsgLevel( QStringLiteral( "Getting column datatype: %1" ).arg( sql ), 2 );
4060       result = connectionRO()->PQexec( sql, false );
4061       QgsDebugMsgLevel( QStringLiteral( "Column datatype query returned %1" ).arg( result.PQntuples() ), 2 );
4062       if ( result.PQntuples() == 1 )
4063       {
4064         geomColType = result.PQgetvalue( 0, 0 );
4065         if ( geomColType == QLatin1String( "geometry" ) )
4066           mSpatialColType = SctGeometry;
4067         else if ( geomColType == QLatin1String( "geography" ) )
4068           mSpatialColType = SctGeography;
4069         else if ( geomColType == QLatin1String( "topogeometry" ) )
4070           mSpatialColType = SctTopoGeometry;
4071         else if ( geomColType == QLatin1String( "pcpatch" ) )
4072           mSpatialColType = SctPcPatch;
4073       }
4074       else
4075       {
4076         connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) );
4077       }
4078     }
4079   }
4080   else
4081   {
4082     sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery );
4083     result = connectionRO()->PQexec( sql );
4084     if ( PGRES_TUPLES_OK == result.PQresultStatus() )
4085     {
4086       sql = QStringLiteral( "SELECT (SELECT t.typname FROM pg_type t WHERE oid = %1), upper(postgis_typmod_type(%2)), postgis_typmod_srid(%2)" )
4087             .arg( QString::number( result.PQftype( 0 ) ), QString::number( result.PQfmod( 0 ) ) );
4088       result = connectionRO()->PQexec( sql, false );
4089       if ( result.PQntuples() == 1 )
4090       {
4091         geomColType  = result.PQgetvalue( 0, 0 );
4092         detectedType = result.PQgetvalue( 0, 1 );
4093         detectedSrid = result.PQgetvalue( 0, 2 );
4094         if ( geomColType == QLatin1String( "geometry" ) )
4095           mSpatialColType = SctGeometry;
4096         else if ( geomColType == QLatin1String( "geography" ) )
4097           mSpatialColType = SctGeography;
4098         else if ( geomColType == QLatin1String( "topogeometry" ) )
4099           mSpatialColType = SctTopoGeometry;
4100         else if ( geomColType == QLatin1String( "pcpatch" ) )
4101           mSpatialColType = SctPcPatch;
4102         else
4103         {
4104           detectedType = mRequestedGeomType == QgsWkbTypes::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType );
4105           detectedSrid = mRequestedSrid;
4106         }
4107       }
4108       else
4109       {
4110         connectionRO()->PQexecNR( QStringLiteral( "COMMIT" ) );
4111         detectedType = mRequestedGeomType == QgsWkbTypes::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType );
4112       }
4113     }
4114     else
4115     {
4116       mValid = false;
4117       return false;
4118     }
4119   }
4120 
4121   mDetectedGeomType = QgsPostgresConn::wkbTypeFromPostgis( detectedType );
4122   mDetectedSrid     = detectedSrid;
4123 
4124   if ( mDetectedGeomType == QgsWkbTypes::Unknown )
4125   {
4126 
4127     QgsPostgresLayerProperty layerProperty;
4128     if ( !mIsQuery )
4129     {
4130       layerProperty.schemaName = schemaName;
4131       layerProperty.tableName  = tableName;
4132     }
4133     else
4134     {
4135       layerProperty.schemaName.clear();
4136       layerProperty.tableName  = mQuery;
4137     }
4138     layerProperty.geometryColName = mGeometryColumn;
4139     layerProperty.geometryColType = mSpatialColType;
4140 
4141     QString delim;
4142 
4143     if ( !mSqlWhereClause.isEmpty() )
4144     {
4145       layerProperty.sql += delim + '(' + mSqlWhereClause + ')';
4146       delim = QStringLiteral( " AND " );
4147     }
4148 
4149     connectionRO()->retrieveLayerTypes( layerProperty, mUseEstimatedMetadata );
4150 
4151     mSpatialColType = layerProperty.geometryColType;
4152 
4153     if ( layerProperty.size() == 0 )
4154     {
4155       // no data - so take what's requested/detected
4156       if ( mRequestedGeomType == QgsWkbTypes::Unknown || mDetectedSrid.isEmpty() )
4157       {
4158         QgsMessageLog::logMessage( tr( "Geometry type and srid for empty column %1 of %2 undefined." ).arg( mGeometryColumn, mQuery ) );
4159       }
4160     }
4161     else
4162     {
4163       int i;
4164       for ( i = 0; i < layerProperty.size(); i++ )
4165       {
4166         QgsWkbTypes::Type wkbType = layerProperty.types.at( i );
4167 
4168         if ( ( wkbType != QgsWkbTypes::Unknown && ( mRequestedGeomType == QgsWkbTypes::Unknown || mRequestedGeomType == wkbType ) ) &&
4169              ( mRequestedSrid.isEmpty() || layerProperty.srids.at( i ) == mRequestedSrid.toInt() ) )
4170           break;
4171       }
4172 
4173       // requested type && srid is available
4174       if ( i < layerProperty.size() )
4175       {
4176         if ( layerProperty.size() == 1 )
4177         {
4178           // only what we requested is available
4179           mDetectedGeomType = layerProperty.types.at( 0 );
4180           mDetectedSrid     = QString::number( layerProperty.srids.at( 0 ) );
4181         }
4182       }
4183       else
4184       {
4185         // geometry type undetermined or not unrequested
4186         QgsMessageLog::logMessage( tr( "Feature type or srid for %1 of %2 could not be determined or was not requested." ).arg( mGeometryColumn, mQuery ) );
4187       }
4188     }
4189   }
4190 
4191   QgsDebugMsgLevel( QStringLiteral( "Detected SRID is %1" ).arg( mDetectedSrid ), 2 );
4192   QgsDebugMsgLevel( QStringLiteral( "Requested SRID is %1" ).arg( mRequestedSrid ), 2 );
4193   QgsDebugMsgLevel( QStringLiteral( "Detected type is %1" ).arg( mDetectedGeomType ), 2 );
4194   QgsDebugMsgLevel( QStringLiteral( "Requested type is %1" ).arg( mRequestedGeomType ), 2 );
4195 
4196   mValid = ( mDetectedGeomType != QgsWkbTypes::Unknown || mRequestedGeomType != QgsWkbTypes::Unknown )
4197            && ( !mDetectedSrid.isEmpty() || !mRequestedSrid.isEmpty() );
4198 
4199   if ( !mValid )
4200     return false;
4201 
4202   QgsDebugMsgLevel( QStringLiteral( "Spatial column type is %1" ).arg( QgsPostgresConn::displayStringForGeomType( mSpatialColType ) ), 2 );
4203 
4204   return mValid;
4205 }
4206 
convertField(QgsField & field,const QMap<QString,QVariant> * options)4207 bool QgsPostgresProvider::convertField( QgsField &field, const QMap<QString, QVariant> *options )
4208 {
4209   //determine field type to use for strings
4210   QString stringFieldType = QStringLiteral( "varchar" );
4211   if ( options && options->value( QStringLiteral( "dropStringConstraints" ), false ).toBool() )
4212   {
4213     //drop string length constraints by using PostgreSQL text type for strings
4214     stringFieldType = QStringLiteral( "text" );
4215   }
4216 
4217   QString fieldType = stringFieldType; //default to string
4218   int fieldSize = field.length();
4219   int fieldPrec = field.precision();
4220   switch ( field.type() )
4221   {
4222     case QVariant::LongLong:
4223       fieldType = QStringLiteral( "int8" );
4224       fieldPrec = 0;
4225       break;
4226 
4227     case QVariant::DateTime:
4228       fieldType = QStringLiteral( "timestamp without time zone" );
4229       break;
4230 
4231     case QVariant::Time:
4232       fieldType = QStringLiteral( "time" );
4233       break;
4234 
4235     case QVariant::String:
4236       fieldType = stringFieldType;
4237       fieldPrec = 0;
4238       break;
4239 
4240     case QVariant::Int:
4241       fieldType = QStringLiteral( "int4" );
4242       fieldPrec = 0;
4243       break;
4244 
4245     case QVariant::Date:
4246       fieldType = QStringLiteral( "date" );
4247       fieldPrec = 0;
4248       break;
4249 
4250     case QVariant::Map:
4251       fieldType = field.typeName();
4252       if ( fieldType.isEmpty() )
4253         fieldType = QStringLiteral( "hstore" );
4254       fieldPrec = 0;
4255       break;
4256 
4257     case QVariant::StringList:
4258       fieldType = QStringLiteral( "_text" );
4259       fieldPrec = 0;
4260       break;
4261 
4262     case QVariant::List:
4263     {
4264       QgsField sub( QString(), field.subType(), QString(), fieldSize, fieldPrec );
4265       if ( !convertField( sub, nullptr ) ) return false;
4266       fieldType = "_" + sub.typeName();
4267       fieldPrec = 0;
4268       break;
4269     }
4270 
4271     case QVariant::Double:
4272       if ( fieldSize > 18 )
4273       {
4274         fieldType = QStringLiteral( "numeric" );
4275         fieldSize = -1;
4276       }
4277       else
4278       {
4279         fieldType = QStringLiteral( "float8" );
4280       }
4281       fieldPrec = 0;
4282       break;
4283 
4284     case QVariant::Bool:
4285       fieldType = QStringLiteral( "bool" );
4286       fieldPrec = 0;
4287       fieldSize = -1;
4288       break;
4289 
4290     case QVariant::ByteArray:
4291       fieldType = QStringLiteral( "bytea" );
4292       fieldPrec = 0;
4293       break;
4294 
4295     default:
4296       return false;
4297   }
4298 
4299   field.setTypeName( fieldType );
4300   field.setLength( fieldSize );
4301   field.setPrecision( fieldPrec );
4302   return true;
4303 }
4304 
4305 
postgisGeometryType(QgsWkbTypes::Type wkbType,QString & geometryType,int & dim)4306 void postgisGeometryType( QgsWkbTypes::Type wkbType, QString &geometryType, int &dim )
4307 {
4308   dim = 2;
4309   QgsWkbTypes::Type flatType = QgsWkbTypes::flatType( wkbType );
4310   geometryType = QgsWkbTypes::displayString( flatType ).toUpper();
4311   switch ( flatType )
4312   {
4313     case QgsWkbTypes::Unknown:
4314       geometryType = QStringLiteral( "GEOMETRY" );
4315       break;
4316 
4317     case QgsWkbTypes::NoGeometry:
4318       geometryType.clear();
4319       dim = 0;
4320       break;
4321 
4322     default:
4323       break;
4324   }
4325 
4326   if ( QgsWkbTypes::hasZ( wkbType ) && QgsWkbTypes::hasM( wkbType ) )
4327   {
4328     dim = 4;
4329   }
4330   else if ( QgsWkbTypes::hasZ( wkbType ) )
4331   {
4332     dim = 3;
4333   }
4334   else if ( QgsWkbTypes::hasM( wkbType ) )
4335   {
4336     geometryType += QLatin1Char( 'M' );
4337     dim = 3;
4338   }
4339   else if ( wkbType >= QgsWkbTypes::Point25D && wkbType <= QgsWkbTypes::MultiPolygon25D )
4340   {
4341     dim = 3;
4342   }
4343 }
4344 
createEmptyLayer(const QString & uri,const QgsFields & fields,QgsWkbTypes::Type wkbType,const QgsCoordinateReferenceSystem & srs,bool overwrite,QMap<int,int> * oldToNewAttrIdxMap,QString * errorMessage,const QMap<QString,QVariant> * options)4345 Qgis::VectorExportResult QgsPostgresProvider::createEmptyLayer( const QString &uri,
4346     const QgsFields &fields,
4347     QgsWkbTypes::Type wkbType,
4348     const QgsCoordinateReferenceSystem &srs,
4349     bool overwrite,
4350     QMap<int, int> *oldToNewAttrIdxMap,
4351     QString *errorMessage,
4352     const QMap<QString, QVariant> *options )
4353 {
4354   // populate members from the uri structure
4355   QgsDataSourceUri dsUri( uri );
4356 
4357   QString schemaName = dsUri.schema();
4358   QString tableName = dsUri.table();
4359 
4360   QString geometryColumn = dsUri.geometryColumn();
4361   QString geometryType;
4362 
4363   QString primaryKey = dsUri.keyColumn();
4364   QString primaryKeyType;
4365 
4366   QStringList pkList;
4367   QStringList pkType;
4368 
4369   QString schemaTableName;
4370   if ( !schemaName.isEmpty() )
4371   {
4372     schemaTableName += quotedIdentifier( schemaName ) + '.';
4373   }
4374   schemaTableName += quotedIdentifier( tableName );
4375 
4376   QgsDebugMsgLevel( QStringLiteral( "Connection info is: %1" ).arg( dsUri.connectionInfo( false ) ), 2 );
4377   QgsDebugMsgLevel( QStringLiteral( "Geometry column is: %1" ).arg( geometryColumn ), 2 );
4378   QgsDebugMsgLevel( QStringLiteral( "Schema is: %1" ).arg( schemaName ), 2 );
4379   QgsDebugMsgLevel( QStringLiteral( "Table name is: %1" ).arg( tableName ), 2 );
4380 
4381   // create the table
4382   QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false );
4383   if ( !conn )
4384   {
4385     if ( errorMessage )
4386       *errorMessage = QObject::tr( "Connection to database failed" );
4387     return Qgis::VectorExportResult::ErrorConnectionFailed;
4388   }
4389 
4390   // get the pk's name and type
4391   // Try to find a PK candidate from numeric NOT NULL / UNIQUE columns
4392   if ( primaryKey.isEmpty() )
4393   {
4394     for ( const auto &field : std::as_const( fields ) )
4395     {
4396       if ( field.isNumeric() &&
4397            ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintUnique ) &&
4398            ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull ) &&
4399            ( field.constraints().constraints() & QgsFieldConstraints::ConstraintOrigin::ConstraintOriginProvider ) )
4400       {
4401         primaryKey = field.name();
4402         break;
4403       }
4404     }
4405   }
4406 
4407   // if no pk name was passed or guessed, define the new pk field name
4408   if ( primaryKey.isEmpty() )
4409   {
4410     int index = 0;
4411     QString pk = primaryKey = QStringLiteral( "id" );
4412     for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx )
4413     {
4414       if ( fields.at( fldIdx ).name() == primaryKey )
4415       {
4416         // it already exists, try again with a new name
4417         primaryKey = QStringLiteral( "%1_%2" ).arg( pk ).arg( index++ );
4418         fldIdx = -1; // it is incremented in the for loop, i.e. restarts at 0
4419       }
4420     }
4421 
4422     pkList = QStringList( primaryKey );
4423     pkType = QStringList( QStringLiteral( "serial" ) );
4424   }
4425   else
4426   {
4427     pkList = parseUriKey( primaryKey );
4428     const auto constPkList = pkList;
4429     for ( const QString &col : constPkList )
4430     {
4431       // search for the passed field
4432       QString type;
4433       for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx )
4434       {
4435         if ( fields[fldIdx].name() == col )
4436         {
4437           // found, get the field type
4438           QgsField fld = fields[fldIdx];
4439           if ( convertField( fld, options ) )
4440           {
4441             type = fld.typeName();
4442             break;
4443           }
4444         }
4445       }
4446       if ( type.isEmpty() ) type = QStringLiteral( "serial" );
4447       else
4448       {
4449         // if the pk field's type is one of the postgres integer types,
4450         // use the equivalent autoincremental type (serialN)
4451         if ( primaryKeyType == QLatin1String( "int2" ) || primaryKeyType == QLatin1String( "int4" ) )
4452         {
4453           primaryKeyType = QStringLiteral( "serial" );
4454         }
4455         else if ( primaryKeyType == QLatin1String( "int8" ) )
4456         {
4457           primaryKeyType = QStringLiteral( "serial8" );
4458         }
4459       }
4460       pkType << type;
4461     }
4462   }
4463 
4464   try
4465   {
4466     conn->PQexecNR( QStringLiteral( "BEGIN" ) );
4467 
4468     // We want a valid schema name ...
4469     if ( schemaName.isEmpty() )
4470     {
4471       QString sql = QString( "SELECT current_schema" );
4472       QgsPostgresResult result( conn->PQexec( sql ) );
4473       if ( result.PQresultStatus() != PGRES_TUPLES_OK )
4474         throw PGException( result );
4475       schemaName = result.PQgetvalue( 0, 0 );
4476       if ( schemaName.isEmpty() )
4477       {
4478         schemaName = QStringLiteral( "public" );
4479       }
4480     }
4481 
4482     QString sql = QString( "SELECT 1"
4483                            " FROM pg_class AS cls JOIN pg_namespace AS nsp"
4484                            " ON nsp.oid=cls.relnamespace "
4485                            " WHERE cls.relname=%1 AND nsp.nspname=%2" )
4486                   .arg( quotedValue( tableName ),
4487                         quotedValue( schemaName ) );
4488 
4489     QgsPostgresResult result( conn->PQexec( sql ) );
4490     if ( result.PQresultStatus() != PGRES_TUPLES_OK )
4491       throw PGException( result );
4492 
4493     bool exists = result.PQntuples() > 0;
4494 
4495     if ( exists && overwrite )
4496     {
4497       // delete the table if exists, then re-create it
4498       QString sql = QString( "SELECT DropGeometryTable(%1,%2)"
4499                              " FROM pg_class AS cls JOIN pg_namespace AS nsp"
4500                              " ON nsp.oid=cls.relnamespace "
4501                              " WHERE cls.relname=%2 AND nsp.nspname=%1" )
4502                     .arg( quotedValue( schemaName ),
4503                           quotedValue( tableName ) );
4504 
4505       result = conn->PQexec( sql );
4506       if ( result.PQresultStatus() != PGRES_TUPLES_OK )
4507         throw PGException( result );
4508     }
4509 
4510     sql = QStringLiteral( "CREATE TABLE %1(" ) .arg( schemaTableName );
4511     QString pk;
4512     for ( int i = 0; i < pkList.size(); ++i )
4513     {
4514       QString col = pkList[i];
4515       const QString &type = pkType[i];
4516 
4517       if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() )
4518       {
4519         col = col.toLower();
4520       }
4521       else
4522       {
4523         col = quotedIdentifier( col ); // no need to quote lowercase field
4524       }
4525 
4526       if ( i )
4527       {
4528         pk  += QLatin1Char( ',' );
4529         sql += QLatin1Char( ',' );
4530       }
4531 
4532       pk += col;
4533       sql += col + " " + type;
4534     }
4535     sql += QStringLiteral( ", PRIMARY KEY (%1) )" ) .arg( pk );
4536 
4537     result = conn->PQexec( sql );
4538     if ( result.PQresultStatus() != PGRES_COMMAND_OK )
4539       throw PGException( result );
4540 
4541     // get geometry type, dim and srid
4542     int dim = 2;
4543     long srid = srs.postgisSrid();
4544 
4545     postgisGeometryType( wkbType, geometryType, dim );
4546 
4547     // create geometry column
4548     if ( !geometryType.isEmpty() )
4549     {
4550       sql = QStringLiteral( "SELECT AddGeometryColumn(%1,%2,%3,%4,%5,%6)" )
4551             .arg( quotedValue( schemaName ),
4552                   quotedValue( tableName ),
4553                   quotedValue( geometryColumn ) )
4554             .arg( srid )
4555             .arg( quotedValue( geometryType ) )
4556             .arg( dim );
4557 
4558       result = conn->PQexec( sql );
4559       if ( result.PQresultStatus() != PGRES_TUPLES_OK )
4560         throw PGException( result );
4561     }
4562     else
4563     {
4564       geometryColumn.clear();
4565     }
4566 
4567     conn->PQexecNR( QStringLiteral( "COMMIT" ) );
4568   }
4569   catch ( PGException &e )
4570   {
4571     if ( errorMessage )
4572       *errorMessage = QObject::tr( "Creation of data source %1 failed: \n%2" )
4573                       .arg( schemaTableName,
4574                             e.errorMessage() );
4575 
4576     conn->PQexecNR( QStringLiteral( "ROLLBACK" ) );
4577     conn->unref();
4578     return Qgis::VectorExportResult::ErrorCreatingLayer;
4579   }
4580   conn->unref();
4581 
4582   QgsDebugMsgLevel( QStringLiteral( "layer %1 created" ).arg( schemaTableName ), 2 );
4583 
4584   // use the provider to edit the table
4585   dsUri.setDataSource( schemaName, tableName, geometryColumn, QString(), primaryKey );
4586 
4587   QgsDataProvider::ProviderOptions providerOptions;
4588   QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags();
4589   std::unique_ptr< QgsPostgresProvider > provider = std::make_unique< QgsPostgresProvider >( dsUri.uri( false ), providerOptions, flags );
4590   if ( !provider->isValid() )
4591   {
4592     if ( errorMessage )
4593       *errorMessage = QObject::tr( "Loading of the layer %1 failed" ).arg( schemaTableName );
4594 
4595     return Qgis::VectorExportResult::ErrorInvalidLayer;
4596   }
4597 
4598   QgsDebugMsgLevel( QStringLiteral( "layer loaded" ), 2 );
4599 
4600   // add fields to the layer
4601   if ( oldToNewAttrIdxMap )
4602     oldToNewAttrIdxMap->clear();
4603 
4604   if ( fields.size() > 0 )
4605   {
4606     int offset = 1;
4607 
4608     // get the list of fields
4609     QList<QgsField> flist;
4610     for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx )
4611     {
4612       QgsField fld = fields.at( fldIdx );
4613 
4614       if ( fld.name() == geometryColumn )
4615       {
4616         //the "lowercaseFieldNames" option does not affect the name of the geometry column, so we perform
4617         //this test before converting the field name to lowercase
4618         QgsDebugMsgLevel( QStringLiteral( "Found a field with the same name of the geometry column. Skip it!" ), 2 );
4619         continue;
4620       }
4621 
4622       if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() )
4623       {
4624         //convert field name to lowercase
4625         fld.setName( fld.name().toLower() );
4626       }
4627 
4628       int pkIdx = -1;
4629       for ( int i = 0; i < pkList.size(); ++i )
4630       {
4631         QString col = pkList[i];
4632         if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() )
4633         {
4634           //convert field name to lowercase (TODO: avoid doing this
4635           //over and over)
4636           col = col.toLower();
4637         }
4638         if ( fld.name() == col )
4639         {
4640           pkIdx = i;
4641           break;
4642         }
4643       }
4644       if ( pkIdx >= 0 )
4645       {
4646         oldToNewAttrIdxMap->insert( fldIdx, pkIdx );
4647         continue;
4648       }
4649 
4650       if ( !( options && options->value( QStringLiteral( "skipConvertFields" ), false ).toBool() ) && !convertField( fld, options ) )
4651       {
4652         if ( errorMessage )
4653           *errorMessage = QObject::tr( "Unsupported type for field %1" ).arg( fld.name() );
4654 
4655         return Qgis::VectorExportResult::ErrorAttributeTypeUnsupported;
4656       }
4657 
4658       QgsDebugMsgLevel( QStringLiteral( "creating field #%1 -> #%2 name %3 type %4 typename %5 width %6 precision %7" )
4659                         .arg( fldIdx ).arg( offset )
4660                         .arg( fld.name(), QVariant::typeToName( fld.type() ), fld.typeName() )
4661                         .arg( fld.length() ).arg( fld.precision() ), 2
4662                       );
4663 
4664       flist.append( fld );
4665       if ( oldToNewAttrIdxMap )
4666         oldToNewAttrIdxMap->insert( fldIdx, offset++ );
4667     }
4668 
4669     if ( !provider->addAttributes( flist ) )
4670     {
4671       if ( errorMessage )
4672         *errorMessage = QObject::tr( "Creation of fields failed:\n%1" ).arg( provider->errors().join( '\n' ) );
4673 
4674       return Qgis::VectorExportResult::ErrorAttributeCreationFailed;
4675     }
4676 
4677     QgsDebugMsgLevel( QStringLiteral( "Done creating fields" ), 2 );
4678   }
4679   return Qgis::VectorExportResult::Success;
4680 }
4681 
crs() const4682 QgsCoordinateReferenceSystem QgsPostgresProvider::crs() const
4683 {
4684   QgsCoordinateReferenceSystem srs;
4685   int srid = mRequestedSrid.isEmpty() ? mDetectedSrid.toInt() : mRequestedSrid.toInt();
4686 
4687   {
4688     static QMutex sMutex;
4689     QMutexLocker locker( &sMutex );
4690     static QMap<int, QgsCoordinateReferenceSystem> sCrsCache;
4691     if ( sCrsCache.contains( srid ) )
4692       srs = sCrsCache.value( srid );
4693     else
4694     {
4695       QgsPostgresConn *conn = connectionRO();
4696       if ( conn )
4697       {
4698         QgsPostgresResult result( conn->PQexec( QStringLiteral( "SELECT auth_name, auth_srid, srtext, proj4text FROM spatial_ref_sys WHERE srid=%1" ).arg( srid ) ) );
4699         if ( result.PQresultStatus() == PGRES_TUPLES_OK )
4700         {
4701           const QString authName = result.PQgetvalue( 0, 0 );
4702           const QString authSRID = result.PQgetvalue( 0, 1 );
4703           const QString srText = result.PQgetvalue( 0, 2 );
4704           bool ok = false;
4705           if ( authName == QLatin1String( "EPSG" ) || authName == QLatin1String( "ESRI" ) )
4706           {
4707             ok = srs.createFromUserInput( authName + ':' + authSRID );
4708           }
4709           if ( !ok && !srText.isEmpty() )
4710           {
4711             ok = srs.createFromUserInput( srText );
4712           }
4713           if ( !ok )
4714             srs = QgsCoordinateReferenceSystem::fromProj( result.PQgetvalue( 0, 3 ) );
4715           sCrsCache.insert( srid, srs );
4716         }
4717       }
4718     }
4719   }
4720   return srs;
4721 }
4722 
subsetString() const4723 QString QgsPostgresProvider::subsetString() const
4724 {
4725   return mSqlWhereClause;
4726 }
4727 
getTableName()4728 QString QgsPostgresProvider::getTableName()
4729 {
4730   return mTableName;
4731 }
4732 
layerCount() const4733 size_t QgsPostgresProvider::layerCount() const
4734 {
4735   return 1;                   // XXX need to return actual number of layers
4736 } // QgsPostgresProvider::layerCount()
4737 
4738 
name() const4739 QString  QgsPostgresProvider::name() const
4740 {
4741   return POSTGRES_KEY;
4742 } //  QgsPostgresProvider::name()
4743 
description() const4744 QString  QgsPostgresProvider::description() const
4745 {
4746   QString pgVersion( tr( "PostgreSQL version: unknown" ) );
4747   QString postgisVersion( tr( "unknown" ) );
4748 
4749   if ( auto *lConnectionRO = connectionRO() )
4750   {
4751     QgsPostgresResult result;
4752 
4753     result = lConnectionRO->PQexec( QStringLiteral( "SELECT version()" ) );
4754     if ( result.PQresultStatus() == PGRES_TUPLES_OK )
4755     {
4756       pgVersion = result.PQgetvalue( 0, 0 );
4757     }
4758 
4759     result = lConnectionRO->PQexec( QStringLiteral( "SELECT postgis_version()" ) );
4760     if ( result.PQresultStatus() == PGRES_TUPLES_OK )
4761     {
4762       postgisVersion = result.PQgetvalue( 0, 0 );
4763     }
4764   }
4765   else
4766   {
4767     pgVersion = tr( "PostgreSQL not connected" );
4768   }
4769 
4770   return tr( "PostgreSQL/PostGIS provider\n%1\nPostGIS %2" ).arg( pgVersion, postgisVersion );
4771 } //  QgsPostgresProvider::description()
4772 
jumpSpace(const QString & txt,int & i)4773 static void jumpSpace( const QString &txt, int &i )
4774 {
4775   while ( i < txt.length() && txt.at( i ).isSpace() )
4776     ++i;
4777 }
4778 
getNextString(const QString & txt,int & i,const QString & sep)4779 QString QgsPostgresProvider::getNextString( const QString &txt, int &i, const QString &sep )
4780 {
4781   jumpSpace( txt, i );
4782   if ( i < txt.length() && txt.at( i ) == '"' )
4783   {
4784     const thread_local QRegularExpression stringRe( QRegularExpression::anchoredPattern( "^\"((?:\\\\.|[^\"\\\\])*)\".*" ) );
4785     const QRegularExpressionMatch match = stringRe.match( txt.mid( i ) );
4786     if ( !match.hasMatch() )
4787     {
4788       QgsMessageLog::logMessage( tr( "Cannot find end of double quoted string: %1" ).arg( txt ), tr( "PostGIS" ) );
4789       return QString();
4790     }
4791     i += match.captured( 1 ).length() + 2;
4792     jumpSpace( txt, i );
4793     if ( !QStringView{txt}.mid( i ).startsWith( sep ) && i < txt.length() )
4794     {
4795       QgsMessageLog::logMessage( tr( "Cannot find separator: %1" ).arg( txt.mid( i ) ), tr( "PostGIS" ) );
4796       return QString();
4797     }
4798     i += sep.length();
4799     return match.captured( 1 ).replace( QLatin1String( "\\\"" ), QLatin1String( "\"" ) ).replace( QLatin1String( "\\\\" ), QLatin1String( "\\" ) );
4800   }
4801   else
4802   {
4803     int start = i;
4804     for ( ; i < txt.length(); i++ )
4805     {
4806       if ( QStringView{txt}.mid( i ).startsWith( sep ) )
4807       {
4808         QStringView v( QStringView{txt}.mid( start, i - start ) );
4809         i += sep.length();
4810         return v.trimmed().toString();
4811       }
4812     }
4813     return QStringView{txt}.mid( start, i - start ).trimmed().toString();
4814   }
4815 }
4816 
parseHstore(const QString & txt)4817 QVariant QgsPostgresProvider::parseHstore( const QString &txt )
4818 {
4819   QVariantMap result;
4820   int i = 0;
4821   while ( i < txt.length() )
4822   {
4823     QString key = getNextString( txt, i, QStringLiteral( "=>" ) );
4824     QString value = getNextString( txt, i, QStringLiteral( "," ) );
4825     if ( key.isNull() || value.isNull() )
4826     {
4827       QgsMessageLog::logMessage( tr( "Error parsing hstore: %1" ).arg( txt ), tr( "PostGIS" ) );
4828       break;
4829     }
4830     result.insert( key, value );
4831   }
4832 
4833   return result;
4834 }
4835 
parseJson(const QString & txt)4836 QVariant QgsPostgresProvider::parseJson( const QString &txt )
4837 {
4838   return QgsJsonUtils::parseJson( txt );
4839 }
4840 
parseOtherArray(const QString & txt,QVariant::Type subType,const QString & typeName)4841 QVariant QgsPostgresProvider::parseOtherArray( const QString &txt, QVariant::Type subType, const QString &typeName )
4842 {
4843   int i = 0;
4844   QVariantList result;
4845   while ( i < txt.length() )
4846   {
4847     const QString value = getNextString( txt, i, QStringLiteral( "," ) );
4848     if ( value.isNull() )
4849     {
4850       QgsMessageLog::logMessage( tr( "Error parsing array: %1" ).arg( txt ), tr( "PostGIS" ) );
4851       break;
4852     }
4853     result.append( QgsPostgresProvider::convertValue( subType, QVariant::Invalid, value, typeName ) );
4854   }
4855   return result;
4856 }
4857 
parseStringArray(const QString & txt)4858 QVariant QgsPostgresProvider::parseStringArray( const QString &txt )
4859 {
4860   int i = 0;
4861   QStringList result;
4862   while ( i < txt.length() )
4863   {
4864     const QString value = getNextString( txt, i, QStringLiteral( "," ) );
4865     if ( value.isNull() )
4866     {
4867       QgsMessageLog::logMessage( tr( "Error parsing array: %1" ).arg( txt ), tr( "PostGIS" ) );
4868       break;
4869     }
4870     result.append( value );
4871   }
4872   return result;
4873 }
4874 
parseMultidimensionalArray(const QString & txt)4875 QVariant QgsPostgresProvider::parseMultidimensionalArray( const QString &txt )
4876 {
4877   QStringList result;
4878   if ( !txt.startsWith( '{' ) || !txt.endsWith( '}' ) )
4879   {
4880     QgsMessageLog::logMessage( tr( "Error parsing array, missing curly braces: %1" ).arg( txt ), tr( "PostGIS" ) );
4881     return result;
4882   }
4883 
4884   QStringList values;
4885   QString text = txt;
4886   while ( !text.isEmpty() )
4887   {
4888     bool escaped = false;
4889     int openedBrackets = 1;
4890     int i = 0;
4891     while ( i < text.length()  && openedBrackets > 0 )
4892     {
4893       ++i;
4894 
4895       if ( text.at( i ) == '}' && !escaped ) openedBrackets--;
4896       else if ( text.at( i ) == '{' && !escaped ) openedBrackets++;
4897 
4898       escaped = !escaped ? text.at( i ) == '\\' : false;
4899     }
4900 
4901     values.append( text.left( ++i ) );
4902     i = text.indexOf( ',', i );
4903     i = i > 0 ? text.indexOf( '{', i ) : -1;
4904     if ( i == -1 )
4905       break;
4906 
4907     text = text.mid( i );
4908   }
4909   return values;
4910 
4911 }
4912 
parseArray(const QString & txt,QVariant::Type type,QVariant::Type subType,const QString & typeName)4913 QVariant QgsPostgresProvider::parseArray( const QString &txt, QVariant::Type type, QVariant::Type subType, const QString &typeName )
4914 {
4915   if ( !txt.startsWith( '{' ) || !txt.endsWith( '}' ) )
4916   {
4917     if ( !txt.isEmpty() )
4918       QgsMessageLog::logMessage( tr( "Error parsing array, missing curly braces: %1" ).arg( txt ), tr( "PostGIS" ) );
4919     return QVariant( type );
4920   }
4921   QString inner = txt.mid( 1, txt.length() - 2 );
4922   if ( ( type == QVariant::StringList || type == QVariant::List ) && inner.startsWith( "{" ) )
4923     return parseMultidimensionalArray( inner );
4924   else if ( type == QVariant::StringList )
4925     return parseStringArray( inner );
4926   else
4927     return parseOtherArray( inner, subType, typeName );
4928 }
4929 
convertValue(QVariant::Type type,QVariant::Type subType,const QString & value,const QString & typeName)4930 QVariant QgsPostgresProvider::convertValue( QVariant::Type type, QVariant::Type subType, const QString &value, const QString &typeName )
4931 {
4932   QVariant result;
4933   switch ( type )
4934   {
4935     case QVariant::Map:
4936       if ( typeName == QLatin1String( "json" ) || typeName == QLatin1String( "jsonb" ) )
4937         result = parseJson( value );
4938       else
4939         result = parseHstore( value );
4940       break;
4941     case QVariant::StringList:
4942     case QVariant::List:
4943       result = parseArray( value, type, subType, typeName );
4944       break;
4945     case QVariant::Bool:
4946       if ( value == QChar( 't' ) )
4947         result = true;
4948       else if ( value == QChar( 'f' ) )
4949         result = false;
4950       else
4951         result = QVariant( type );
4952       break;
4953     default:
4954       result = value;
4955       if ( !result.convert( type ) || value.isNull() )
4956         result = QVariant( type );
4957       break;
4958   }
4959 
4960   return result;
4961 }
4962 
searchLayers(const QList<QgsVectorLayer * > & layers,const QString & connectionInfo,const QString & schema,const QString & tableName)4963 QList<QgsVectorLayer *> QgsPostgresProvider::searchLayers( const QList<QgsVectorLayer *> &layers, const QString &connectionInfo, const QString &schema, const QString &tableName )
4964 {
4965   QList<QgsVectorLayer *> result;
4966   const auto constLayers = layers;
4967   for ( QgsVectorLayer *layer : constLayers )
4968   {
4969     const QgsPostgresProvider *pgProvider = qobject_cast<QgsPostgresProvider *>( layer->dataProvider() );
4970     if ( pgProvider &&
4971          pgProvider->mUri.connectionInfo( false ) == connectionInfo && pgProvider->mSchemaName == schema && pgProvider->mTableName == tableName )
4972     {
4973       result.append( layer );
4974     }
4975   }
4976   return result;
4977 }
4978 
discoverRelations(const QgsVectorLayer * target,const QList<QgsVectorLayer * > & layers) const4979 QList<QgsRelation> QgsPostgresProvider::discoverRelations( const QgsVectorLayer *target, const QList<QgsVectorLayer *> &layers ) const
4980 {
4981   QList<QgsRelation> result;
4982 
4983   // Silently skip if this is a query layer or for some obscure reason there are no table and schema name
4984   if ( mIsQuery || mTableName.isEmpty() || mSchemaName.isEmpty() )
4985   {
4986     return result;
4987   }
4988 
4989   // Skip less silently if layer is not valid
4990   if ( !mValid )
4991   {
4992     QgsLogger::warning( tr( "Error discovering relations of %1: invalid layer" ).arg( mQuery ) );
4993     return result;
4994   }
4995 
4996 
4997   QString sql(
4998     "WITH foreign_keys AS "
4999     "  ( SELECT c.conname, "
5000     "           c.conrelid, "
5001     "           c.confrelid, "
5002     "           unnest(c.conkey) AS conkey, "
5003     "           unnest(c.confkey) AS confkey, "
5004     "     (SELECT relname "
5005     "      FROM pg_catalog.pg_class "
5006     "      WHERE oid = c.conrelid) as referencing_table, "
5007     "     (SELECT relname "
5008     "      FROM pg_catalog.pg_class "
5009     "      WHERE oid = c.confrelid) as referenced_table, "
5010     "     (SELECT relnamespace::regnamespace::text "
5011     "      FROM pg_catalog.pg_class "
5012     "      WHERE oid = c.confrelid) as constraint_schema "
5013     "   FROM pg_constraint c "
5014     "   WHERE contype = 'f' "
5015     "     AND c.conrelid::regclass = " +
5016     QgsPostgresConn::quotedValue( QString( QgsPostgresConn::quotedIdentifier( mSchemaName ) +
5017                                   '.' +
5018                                   QgsPostgresConn::quotedIdentifier( mTableName ) ) ) +
5019     "::regclass ) "
5020     "SELECT fk.conname as constraint_name, "
5021     "       a.attname as column_name, "
5022     "       fk.constraint_schema, "
5023     "       referenced_table as table_name, "
5024     "       af.attname as column_name, "
5025     "       fk.confkey as ordinal_position "
5026     "FROM foreign_keys fk "
5027     "JOIN pg_attribute af ON af.attnum = fk.confkey "
5028     "AND af.attrelid = fk.confrelid "
5029     "JOIN pg_attribute a ON a.attnum = conkey "
5030     "AND a.attrelid = fk.conrelid "
5031     "ORDER BY fk.confrelid, "
5032     "         fk.conname ;"
5033   );
5034 
5035   QgsPostgresResult sqlResult( connectionRO()->PQexec( sql ) );
5036   if ( sqlResult.PQresultStatus() != PGRES_TUPLES_OK )
5037   {
5038     QgsLogger::warning( "Error getting the foreign keys of " + mTableName );
5039     return result;
5040   }
5041 
5042   int nbFound = 0;
5043   QList<QString> refTableFound;
5044   for ( int row = 0; row < sqlResult.PQntuples(); ++row )
5045   {
5046     const QString name = sqlResult.PQgetvalue( row, 0 );
5047     const QString fkColumn = sqlResult.PQgetvalue( row, 1 );
5048     QString refSchema = sqlResult.PQgetvalue( row, 2 );
5049     QString refTable = sqlResult.PQgetvalue( row, 3 );
5050     // Strip quotes
5051     if ( refTable.startsWith( '"' ) && refTable.endsWith( '"' ) )
5052     {
5053       refTable = refTable.mid( 1, refTable.length() - 2 );
5054     }
5055     if ( refSchema.startsWith( '"' ) && refSchema.endsWith( '"' ) )
5056     {
5057       refSchema = refSchema.mid( 1, refSchema.length() - 2 );
5058     }
5059     const QString refColumn = sqlResult.PQgetvalue( row, 4 );
5060     const QString position = sqlResult.PQgetvalue( row, 5 );
5061     if ( ( position == QLatin1String( "1" ) ) || ( nbFound == 0 ) || ( !refTableFound.contains( refTable ) ) )
5062     {
5063       // first reference field => try to find if we have layers for the referenced table
5064       const QList<QgsVectorLayer *> foundLayers = searchLayers( layers, mUri.connectionInfo( false ), refSchema, refTable );
5065       const auto constFoundLayers = foundLayers;
5066       for ( const QgsVectorLayer *foundLayer : constFoundLayers )
5067       {
5068         QgsRelation relation;
5069         relation.setName( name );
5070         relation.setReferencingLayer( target->id() );
5071         relation.setReferencedLayer( foundLayer->id() );
5072         relation.addFieldPair( fkColumn, refColumn );
5073         relation.generateId();
5074         if ( relation.isValid() )
5075         {
5076           result.append( relation );
5077           ++nbFound;
5078           refTableFound.append( refTable );
5079         }
5080         else
5081         {
5082           QgsLogger::warning( "Invalid relation for " + name );
5083         }
5084       }
5085     }
5086     else
5087     {
5088       // multi reference field => add the field pair to all the referenced layers found
5089       const QList<QgsVectorLayer *> foundLayers = searchLayers( layers, mUri.connectionInfo( false ), refSchema, refTable );
5090       for ( int i = 0; i < nbFound; ++i )
5091       {
5092         for ( const QgsVectorLayer *foundLayer : foundLayers )
5093         {
5094           if ( result[result.size() - 1 - i].referencedLayerId() == foundLayer->id() )
5095           {
5096             result[result.size() - 1 - i].addFieldPair( fkColumn, refColumn );
5097           }
5098         }
5099       }
5100     }
5101   }
5102   return result;
5103 }
5104 
palAttributeIndexNames() const5105 QgsAttrPalIndexNameHash QgsPostgresProvider::palAttributeIndexNames() const
5106 {
5107   return mAttrPalIndexName;
5108 }
5109 
setQuery(const QString & query)5110 void QgsPostgresProvider::setQuery( const QString &query )
5111 {
5112   mQuery = query;
5113 
5114   mKind = Relkind::NotSet;
5115 }
5116 
relkind() const5117 QgsPostgresProvider::Relkind QgsPostgresProvider::relkind() const
5118 {
5119   if ( mKind != Relkind::NotSet )
5120     return mKind;
5121 
5122   if ( mIsQuery || !connectionRO() )
5123   {
5124     mKind = Relkind::Unknown;
5125   }
5126   else
5127   {
5128     QString sql = QStringLiteral( "SELECT relkind FROM pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) );
5129     QgsPostgresResult res( connectionRO()->PQexec( sql ) );
5130     QString type = res.PQgetvalue( 0, 0 );
5131 
5132     mKind = Relkind::Unknown;
5133 
5134     if ( type == QLatin1String( "r" ) )
5135     {
5136       mKind = Relkind::OrdinaryTable;
5137     }
5138     else if ( type == QLatin1String( "i" ) )
5139     {
5140       mKind = Relkind::Index;
5141     }
5142     else if ( type == QLatin1String( "s" ) )
5143     {
5144       mKind = Relkind::Sequence;
5145     }
5146     else if ( type == QLatin1String( "v" ) )
5147     {
5148       mKind = Relkind::View;
5149     }
5150     else if ( type == QLatin1String( "m" ) )
5151     {
5152       mKind = Relkind::MaterializedView;
5153     }
5154     else if ( type == QLatin1String( "c" ) )
5155     {
5156       mKind = Relkind::CompositeType;
5157     }
5158     else if ( type == QLatin1String( "t" ) )
5159     {
5160       mKind = Relkind::ToastTable;
5161     }
5162     else if ( type == QLatin1String( "f" ) )
5163     {
5164       mKind = Relkind::ForeignTable;
5165     }
5166     else if ( type == QLatin1String( "p" ) )
5167     {
5168       mKind = Relkind::PartitionedTable;
5169     }
5170   }
5171 
5172   return mKind;
5173 }
5174 
hasMetadata() const5175 bool QgsPostgresProvider::hasMetadata() const
5176 {
5177   bool hasMetadata = true;
5178   QgsPostgresProvider::Relkind kind = relkind();
5179 
5180   if ( kind == Relkind::View || kind == Relkind::MaterializedView )
5181   {
5182     hasMetadata = false;
5183   }
5184 
5185   return hasMetadata;
5186 }
5187 
createProvider(const QString & uri,const QgsDataProvider::ProviderOptions & options,QgsDataProvider::ReadFlags flags)5188 QgsDataProvider *QgsPostgresProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags )
5189 {
5190   return new QgsPostgresProvider( uri, options, flags );
5191 }
5192 
dataItemProviders() const5193 QList< QgsDataItemProvider * > QgsPostgresProviderMetadata::dataItemProviders() const
5194 {
5195   QList<QgsDataItemProvider *> providers;
5196   providers << new QgsPostgresDataItemProvider;
5197   return providers;
5198 }
5199 
5200 // ---------------------------------------------------------------------------
5201 
createEmptyLayer(const QString & uri,const QgsFields & fields,QgsWkbTypes::Type wkbType,const QgsCoordinateReferenceSystem & srs,bool overwrite,QMap<int,int> & oldToNewAttrIdxMap,QString & errorMessage,const QMap<QString,QVariant> * options)5202 Qgis::VectorExportResult QgsPostgresProviderMetadata::createEmptyLayer(
5203   const QString &uri,
5204   const QgsFields &fields,
5205   QgsWkbTypes::Type wkbType,
5206   const QgsCoordinateReferenceSystem &srs,
5207   bool overwrite,
5208   QMap<int, int> &oldToNewAttrIdxMap,
5209   QString &errorMessage,
5210   const QMap<QString, QVariant> *options )
5211 {
5212   return QgsPostgresProvider::createEmptyLayer(
5213            uri, fields, wkbType, srs, overwrite,
5214            &oldToNewAttrIdxMap, &errorMessage, options
5215          );
5216 }
5217 
saveStyle(const QString & uri,const QString & qmlStyleIn,const QString & sldStyleIn,const QString & styleName,const QString & styleDescription,const QString & uiFileContent,bool useAsDefault,QString & errCause)5218 bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString &qmlStyleIn, const QString &sldStyleIn,
5219     const QString &styleName, const QString &styleDescription,
5220     const QString &uiFileContent, bool useAsDefault, QString &errCause )
5221 {
5222   QgsDataSourceUri dsUri( uri );
5223 
5224   // Replace invalid XML characters
5225   QString qmlStyle { qmlStyleIn };
5226   QgsPostgresUtils::replaceInvalidXmlChars( qmlStyle );
5227   QString sldStyle { sldStyleIn };
5228   QgsPostgresUtils::replaceInvalidXmlChars( sldStyle );
5229 
5230   QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false );
5231   if ( !conn )
5232   {
5233     errCause = QObject::tr( "Connection to database failed" );
5234     return false;
5235   }
5236 
5237   if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) )
5238   {
5239     QgsPostgresResult res( conn->PQexec( "CREATE TABLE layer_styles("
5240                                          "id SERIAL PRIMARY KEY"
5241                                          ",f_table_catalog varchar"
5242                                          ",f_table_schema varchar"
5243                                          ",f_table_name varchar"
5244                                          ",f_geometry_column varchar"
5245                                          ",styleName text"
5246                                          ",styleQML xml"
5247                                          ",styleSLD xml"
5248                                          ",useAsDefault boolean"
5249                                          ",description text"
5250                                          ",owner varchar(63) DEFAULT CURRENT_USER"
5251                                          ",ui xml"
5252                                          ",update_time timestamp DEFAULT CURRENT_TIMESTAMP"
5253                                          ",type varchar"
5254                                          ")" ) );
5255     if ( res.PQresultStatus() != PGRES_COMMAND_OK )
5256     {
5257       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() );
5258       conn->unref();
5259       return false;
5260     }
5261   }
5262   else
5263   {
5264     if ( !columnExists( *conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) )
5265     {
5266       QgsPostgresResult res( conn->PQexec( "ALTER TABLE layer_styles ADD COLUMN type varchar NULL" ) );
5267       if ( res.PQresultStatus() != PGRES_COMMAND_OK )
5268       {
5269         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() );
5270         conn->unref();
5271         return false;
5272       }
5273     }
5274   }
5275 
5276   if ( dsUri.database().isEmpty() ) // typically when a service file is used
5277   {
5278     dsUri.setDatabase( conn->currentDatabase() );
5279   }
5280 
5281   QString uiFileColumn;
5282   QString uiFileValue;
5283   if ( !uiFileContent.isEmpty() )
5284   {
5285     uiFileColumn = QStringLiteral( ",ui" );
5286     uiFileValue = QStringLiteral( ",XMLPARSE(DOCUMENT %1)" ).arg( QgsPostgresConn::quotedValue( uiFileContent ) );
5287   }
5288 
5289   const QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
5290 
5291   // Note: in the construction of the INSERT and UPDATE strings the qmlStyle and sldStyle values
5292   // can contain user entered strings, which may themselves include %## values that would be
5293   // replaced by the QString.arg function.  To ensure that the final SQL string is not corrupt these
5294   // two values are both replaced in the final .arg call of the string construction.
5295 
5296   QString sql = QString( "INSERT INTO layer_styles("
5297                          "f_table_catalog,f_table_schema,f_table_name,f_geometry_column,styleName,styleQML,styleSLD,useAsDefault,description,owner,type%12"
5298                          ") VALUES ("
5299                          "%1,%2,%3,%4,%5,XMLPARSE(DOCUMENT %16),XMLPARSE(DOCUMENT %17),%8,%9,%10,%11%13"
5300                          ")" )
5301                 .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5302                 .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5303                 .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5304                 .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) )
5305                 .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) )
5306                 .arg( useAsDefault ? "true" : "false" )
5307                 .arg( QgsPostgresConn::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
5308                 .arg( "CURRENT_USER" )
5309                 .arg( uiFileColumn )
5310                 .arg( uiFileValue )
5311                 .arg( wkbTypeString )
5312                 // Must be the final .arg replacement - see above
5313                 .arg( QgsPostgresConn::quotedValue( qmlStyle ),
5314                       QgsPostgresConn::quotedValue( sldStyle ) );
5315 
5316   QString checkQuery = QString( "SELECT styleName"
5317                                 " FROM layer_styles"
5318                                 " WHERE f_table_catalog=%1"
5319                                 " AND f_table_schema=%2"
5320                                 " AND f_table_name=%3"
5321                                 " AND f_geometry_column=%4"
5322                                 " AND (type=%5 OR type IS NULL)"
5323                                 " AND styleName=%6" )
5324                        .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5325                        .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5326                        .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5327                        .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) )
5328                        .arg( wkbTypeString )
5329                        .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
5330 
5331   QgsPostgresResult res( conn->PQexec( checkQuery ) );
5332   if ( res.PQntuples() > 0 )
5333   {
5334     if ( QMessageBox::question( nullptr, QObject::tr( "Save style in database" ),
5335                                 QObject::tr( "A style named \"%1\" already exists in the database for this layer. Do you want to overwrite it?" )
5336                                 .arg( styleName.isEmpty() ? dsUri.table() : styleName ),
5337                                 QMessageBox::Yes | QMessageBox::No ) == QMessageBox::No )
5338     {
5339       errCause = QObject::tr( "Operation aborted. No changes were made in the database" );
5340       conn->unref();
5341       return false;
5342     }
5343 
5344     sql = QString( "UPDATE layer_styles"
5345                    " SET useAsDefault=%1"
5346                    ",styleQML=XMLPARSE(DOCUMENT %12)"
5347                    ",styleSLD=XMLPARSE(DOCUMENT %13)"
5348                    ",description=%4"
5349                    ",owner=%5"
5350                    ",type=%2"
5351                    " WHERE f_table_catalog=%6"
5352                    " AND f_table_schema=%7"
5353                    " AND f_table_name=%8"
5354                    " AND f_geometry_column=%9"
5355                    " AND styleName=%10"
5356                    " AND (type=%2 OR type IS NULL)" )
5357           .arg( useAsDefault ? "true" : "false" )
5358           .arg( wkbTypeString )
5359           .arg( QgsPostgresConn::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
5360           .arg( "CURRENT_USER" )
5361           .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5362           .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5363           .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5364           .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) )
5365           .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) )
5366           // Must be the final .arg replacement - see above
5367           .arg( QgsPostgresConn::quotedValue( qmlStyle ),
5368                 QgsPostgresConn::quotedValue( sldStyle ) );
5369   }
5370 
5371   if ( useAsDefault )
5372   {
5373     QString removeDefaultSql = QString( "UPDATE layer_styles"
5374                                         " SET useAsDefault=false"
5375                                         " WHERE f_table_catalog=%1"
5376                                         " AND f_table_schema=%2"
5377                                         " AND f_table_name=%3"
5378                                         " AND f_geometry_column=%4"
5379                                         " AND (type=%5 OR type IS NULL)" )
5380                                .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5381                                .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5382                                .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5383                                .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) )
5384                                .arg( wkbTypeString );
5385 
5386     sql = QStringLiteral( "BEGIN; %1; %2; COMMIT;" ).arg( removeDefaultSql, sql );
5387   }
5388 
5389   res = conn->PQexec( sql );
5390 
5391   bool saved = res.PQresultStatus() == PGRES_COMMAND_OK;
5392   if ( !saved )
5393     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() );
5394 
5395   conn->unref();
5396 
5397   return saved;
5398 }
5399 
5400 
loadStyle(const QString & uri,QString & errCause)5401 QString QgsPostgresProviderMetadata::loadStyle( const QString &uri, QString &errCause )
5402 {
5403   QgsDataSourceUri dsUri( uri );
5404   QString selectQmlQuery;
5405 
5406   QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false );
5407   if ( !conn )
5408   {
5409     errCause = QObject::tr( "Connection to database failed" );
5410     return QString();
5411   }
5412 
5413   if ( dsUri.database().isEmpty() ) // typically when a service file is used
5414   {
5415     dsUri.setDatabase( conn->currentDatabase() );
5416   }
5417 
5418   if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) )
5419   {
5420     conn->unref();
5421     return QString();
5422   }
5423 
5424   QString geomColumnExpr;
5425   if ( dsUri.geometryColumn().isEmpty() )
5426   {
5427     geomColumnExpr = QStringLiteral( "IS NULL" );
5428   }
5429   else
5430   {
5431     geomColumnExpr = QStringLiteral( "=" ) + QgsPostgresConn::quotedValue( dsUri.geometryColumn() );
5432   }
5433 
5434   QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
5435 
5436   // support layer_styles without type column < 3.14
5437   if ( !columnExists( *conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) )
5438   {
5439     selectQmlQuery = QString( "SELECT styleQML"
5440                               " FROM layer_styles"
5441                               " WHERE f_table_catalog=%1"
5442                               " AND f_table_schema=%2"
5443                               " AND f_table_name=%3"
5444                               " AND f_geometry_column %4"
5445                               " ORDER BY CASE WHEN useAsDefault THEN 1 ELSE 2 END"
5446                               ",update_time DESC LIMIT 1" )
5447                      .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5448                      .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5449                      .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5450                      .arg( geomColumnExpr );
5451   }
5452   else
5453   {
5454     selectQmlQuery = QString( "SELECT styleQML"
5455                               " FROM layer_styles"
5456                               " WHERE f_table_catalog=%1"
5457                               " AND f_table_schema=%2"
5458                               " AND f_table_name=%3"
5459                               " AND f_geometry_column %4"
5460                               " AND (type=%5 OR type IS NULL)"
5461                               " ORDER BY CASE WHEN useAsDefault THEN 1 ELSE 2 END"
5462                               ",update_time DESC LIMIT 1" )
5463                      .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5464                      .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5465                      .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5466                      .arg( geomColumnExpr )
5467                      .arg( wkbTypeString );
5468   }
5469 
5470   QgsPostgresResult result( conn->PQexec( selectQmlQuery ) );
5471 
5472   QString style = result.PQntuples() == 1 ? result.PQgetvalue( 0, 0 ) : QString();
5473   conn->unref();
5474 
5475   QgsPostgresUtils::restoreInvalidXmlChars( style );
5476 
5477   return style;
5478 }
5479 
listStyles(const QString & uri,QStringList & ids,QStringList & names,QStringList & descriptions,QString & errCause)5480 int QgsPostgresProviderMetadata::listStyles( const QString &uri, QStringList &ids, QStringList &names,
5481     QStringList &descriptions, QString &errCause )
5482 {
5483   QgsDataSourceUri dsUri( uri );
5484 
5485   QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false );
5486   if ( !conn )
5487   {
5488     errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() );
5489     return -1;
5490   }
5491 
5492   if ( dsUri.database().isEmpty() ) // typically when a service file is used
5493   {
5494     dsUri.setDatabase( conn->currentDatabase() );
5495   }
5496 
5497   QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
5498 
5499   QString selectRelatedQuery = QString( "SELECT id,styleName,description"
5500                                         " FROM layer_styles"
5501                                         " WHERE f_table_catalog=%1"
5502                                         " AND f_table_schema=%2"
5503                                         " AND f_table_name=%3"
5504                                         " AND %4"
5505                                         " AND (type=%5 OR type IS NULL)"
5506                                         " ORDER BY useasdefault DESC, update_time DESC" )
5507                                .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5508                                .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5509                                .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5510                                .arg( dsUri.geometryColumn().isEmpty() ? "f_geometry_column is NULL" :
5511                                      QString( "f_geometry_column=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
5512                                .arg( wkbTypeString );
5513 
5514   QgsPostgresResult result( conn->PQexec( selectRelatedQuery ) );
5515   if ( result.PQresultStatus() != PGRES_TUPLES_OK )
5516   {
5517     QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectRelatedQuery ) );
5518     errCause = QObject::tr( "Error executing the select query for related styles. The query was logged" );
5519     conn->unref();
5520     return -1;
5521   }
5522 
5523   int numberOfRelatedStyles = result.PQntuples();
5524   for ( int i = 0; i < numberOfRelatedStyles; i++ )
5525   {
5526     ids.append( result.PQgetvalue( i, 0 ) );
5527     names.append( result.PQgetvalue( i, 1 ) );
5528     descriptions.append( result.PQgetvalue( i, 2 ) );
5529   }
5530 
5531   QString selectOthersQuery = QString( "SELECT id,styleName,description"
5532                                        " FROM layer_styles"
5533                                        " WHERE NOT (f_table_catalog=%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4 AND type=%5)"
5534                                        " ORDER BY update_time DESC" )
5535                               .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5536                               .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5537                               .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5538                               .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) )
5539                               .arg( wkbTypeString );
5540 
5541   result = conn->PQexec( selectOthersQuery );
5542   if ( result.PQresultStatus() != PGRES_TUPLES_OK )
5543   {
5544     QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectOthersQuery ) );
5545     errCause = QObject::tr( "Error executing the select query for unrelated styles. The query was logged" );
5546     conn->unref();
5547     return -1;
5548   }
5549 
5550   for ( int i = 0; i < result.PQntuples(); i++ )
5551   {
5552     ids.append( result.PQgetvalue( i, 0 ) );
5553     names.append( result.PQgetvalue( i, 1 ) );
5554     descriptions.append( result.PQgetvalue( i, 2 ) );
5555   }
5556 
5557   conn->unref();
5558 
5559   return numberOfRelatedStyles;
5560 }
5561 
deleteStyleById(const QString & uri,QString styleId,QString & errCause)5562 bool QgsPostgresProviderMetadata::deleteStyleById( const QString &uri, QString styleId, QString &errCause )
5563 {
5564   QgsDataSourceUri dsUri( uri );
5565   bool deleted;
5566 
5567   QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false );
5568   if ( !conn )
5569   {
5570     errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() );
5571     deleted = false;
5572   }
5573   else
5574   {
5575     QString deleteStyleQuery = QStringLiteral( "DELETE FROM layer_styles WHERE id=%1" ).arg(
5576                                  QgsPostgresConn::quotedValue( styleId ) );
5577     QgsPostgresResult result( conn->PQexec( deleteStyleQuery ) );
5578     if ( result.PQresultStatus() != PGRES_COMMAND_OK )
5579     {
5580       QgsDebugMsg(
5581         QString( "PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
5582         .arg( result.PQresultStatus() ).arg( PGRES_COMMAND_OK ).arg( deleteStyleQuery ) );
5583       QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( deleteStyleQuery ) );
5584       errCause = QObject::tr( "Error executing the delete query. The query was logged" );
5585       deleted = false;
5586     }
5587     else
5588     {
5589       deleted = true;
5590     }
5591     conn->unref();
5592   }
5593   return deleted;
5594 }
5595 
getStyleById(const QString & uri,QString styleId,QString & errCause)5596 QString QgsPostgresProviderMetadata::getStyleById( const QString &uri, QString styleId, QString &errCause )
5597 {
5598   QgsDataSourceUri dsUri( uri );
5599 
5600   QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false );
5601   if ( !conn )
5602   {
5603     errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() );
5604     return QString();
5605   }
5606 
5607   QString style;
5608   QString selectQmlQuery = QStringLiteral( "SELECT styleQml FROM layer_styles WHERE id=%1" ).arg( QgsPostgresConn::quotedValue( styleId ) );
5609   QgsPostgresResult result( conn->PQexec( selectQmlQuery ) );
5610   if ( result.PQresultStatus() == PGRES_TUPLES_OK )
5611   {
5612     if ( result.PQntuples() == 1 )
5613       style = result.PQgetvalue( 0, 0 );
5614     else
5615       errCause = QObject::tr( "Consistency error in table '%1'. Style id should be unique" ).arg( QLatin1String( "layer_styles" ) );
5616   }
5617   else
5618   {
5619     QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectQmlQuery ) );
5620     errCause = QObject::tr( "Error executing the select query. The query was logged" );
5621   }
5622 
5623   conn->unref();
5624 
5625   QgsPostgresUtils::restoreInvalidXmlChars( style );
5626 
5627   return style;
5628 }
5629 
createTransaction(const QString & connString)5630 QgsTransaction *QgsPostgresProviderMetadata::createTransaction( const QString &connString )
5631 {
5632   return new QgsPostgresTransaction( connString );
5633 }
5634 
connections(bool cached)5635 QMap<QString, QgsAbstractProviderConnection *> QgsPostgresProviderMetadata::connections( bool cached )
5636 {
5637   return connectionsProtected<QgsPostgresProviderConnection, QgsPostgresConn>( cached );
5638 }
5639 
createConnection(const QString & uri,const QVariantMap & configuration)5640 QgsAbstractProviderConnection *QgsPostgresProviderMetadata::createConnection( const QString &uri, const QVariantMap &configuration )
5641 {
5642   return new QgsPostgresProviderConnection( uri, configuration );
5643 }
5644 
deleteConnection(const QString & name)5645 void QgsPostgresProviderMetadata::deleteConnection( const QString &name )
5646 {
5647   deleteConnectionProtected<QgsPostgresProviderConnection>( name );
5648 }
5649 
saveConnection(const QgsAbstractProviderConnection * conn,const QString & name)5650 void QgsPostgresProviderMetadata::saveConnection( const QgsAbstractProviderConnection *conn,  const QString &name )
5651 {
5652   saveConnectionProtected( conn, name );
5653 }
5654 
createConnection(const QString & name)5655 QgsAbstractProviderConnection *QgsPostgresProviderMetadata::createConnection( const QString &name )
5656 {
5657   return new QgsPostgresProviderConnection( name );
5658 }
5659 
5660 
5661 QgsPostgresProjectStorage *gPgProjectStorage = nullptr;   // when not null it is owned by QgsApplication::projectStorageRegistry()
5662 
initProvider()5663 void QgsPostgresProviderMetadata::initProvider()
5664 {
5665   Q_ASSERT( !gPgProjectStorage );
5666   gPgProjectStorage = new QgsPostgresProjectStorage;
5667   QgsApplication::projectStorageRegistry()->registerProjectStorage( gPgProjectStorage );  // takes ownership
5668 }
5669 
cleanupProvider()5670 void QgsPostgresProviderMetadata::cleanupProvider()
5671 {
5672   QgsApplication::projectStorageRegistry()->unregisterProjectStorage( gPgProjectStorage );  // destroys the object
5673   gPgProjectStorage = nullptr;
5674 
5675   QgsPostgresConnPool::cleanupInstance();
5676 }
5677 
5678 
5679 // ----------
5680 
addFeaturesCounted(long long diff)5681 void QgsPostgresSharedData::addFeaturesCounted( long long diff )
5682 {
5683   QMutexLocker locker( &mMutex );
5684 
5685   if ( mFeaturesCounted >= 0 )
5686     mFeaturesCounted += diff;
5687 }
5688 
ensureFeaturesCountedAtLeast(long long fetched)5689 void QgsPostgresSharedData::ensureFeaturesCountedAtLeast( long long fetched )
5690 {
5691   QMutexLocker locker( &mMutex );
5692 
5693   /* only updates the feature count if it was already once.
5694    * Otherwise, this would lead to false feature count if
5695    * an existing project is open at a restrictive extent.
5696    */
5697   if ( mFeaturesCounted > 0 && mFeaturesCounted < fetched )
5698   {
5699     QgsDebugMsgLevel( QStringLiteral( "feature count adjusted from %1 to %2" ).arg( mFeaturesCounted ).arg( fetched ), 2 );
5700     mFeaturesCounted = fetched;
5701   }
5702 }
5703 
featuresCounted()5704 long long QgsPostgresSharedData::featuresCounted()
5705 {
5706   QMutexLocker locker( &mMutex );
5707   return mFeaturesCounted;
5708 }
5709 
setFeaturesCounted(long long count)5710 void QgsPostgresSharedData::setFeaturesCounted( long long count )
5711 {
5712   QMutexLocker locker( &mMutex );
5713   mFeaturesCounted = count;
5714 }
5715 
5716 
lookupFid(const QVariantList & v)5717 QgsFeatureId QgsPostgresSharedData::lookupFid( const QVariantList &v )
5718 {
5719   QMutexLocker locker( &mMutex );
5720 
5721   QMap<QVariantList, QgsFeatureId>::const_iterator it = mKeyToFid.constFind( v );
5722 
5723   if ( it != mKeyToFid.constEnd() )
5724   {
5725     return it.value();
5726   }
5727 
5728   mFidToKey.insert( ++mFidCounter, v );
5729   mKeyToFid.insert( v, mFidCounter );
5730 
5731   return mFidCounter;
5732 }
5733 
5734 
removeFid(QgsFeatureId fid)5735 QVariantList QgsPostgresSharedData::removeFid( QgsFeatureId fid )
5736 {
5737   QMutexLocker locker( &mMutex );
5738 
5739   QVariantList v = mFidToKey[ fid ];
5740   mFidToKey.remove( fid );
5741   mKeyToFid.remove( v );
5742   return v;
5743 }
5744 
insertFid(QgsFeatureId fid,const QVariantList & k)5745 void QgsPostgresSharedData::insertFid( QgsFeatureId fid, const QVariantList &k )
5746 {
5747   QMutexLocker locker( &mMutex );
5748 
5749   mFidToKey.insert( fid, k );
5750   mKeyToFid.insert( k, fid );
5751 }
5752 
lookupKey(QgsFeatureId featureId)5753 QVariantList QgsPostgresSharedData::lookupKey( QgsFeatureId featureId )
5754 {
5755   QMutexLocker locker( &mMutex );
5756 
5757   QMap<QgsFeatureId, QVariantList>::const_iterator it = mFidToKey.constFind( featureId );
5758   if ( it != mFidToKey.constEnd() )
5759     return it.value();
5760   return QVariantList();
5761 }
5762 
clear()5763 void QgsPostgresSharedData::clear()
5764 {
5765   QMutexLocker locker( &mMutex );
5766   mFidToKey.clear();
5767   mKeyToFid.clear();
5768   mFeaturesCounted = -1;
5769   mFidCounter = 0;
5770 }
5771 
clearSupportsEnumValuesCache()5772 void QgsPostgresSharedData::clearSupportsEnumValuesCache()
5773 {
5774   QMutexLocker locker( &mMutex );
5775   mFieldSupportsEnumValues.clear();
5776 }
5777 
fieldSupportsEnumValuesIsSet(int index)5778 bool QgsPostgresSharedData::fieldSupportsEnumValuesIsSet( int index )
5779 {
5780   QMutexLocker locker( &mMutex );
5781   return mFieldSupportsEnumValues.contains( index );
5782 }
5783 
fieldSupportsEnumValues(int index)5784 bool QgsPostgresSharedData::fieldSupportsEnumValues( int index )
5785 {
5786   QMutexLocker locker( &mMutex );
5787   return mFieldSupportsEnumValues.contains( index ) && mFieldSupportsEnumValues[ index ];
5788 }
5789 
setFieldSupportsEnumValues(int index,bool isSupported)5790 void QgsPostgresSharedData::setFieldSupportsEnumValues( int index, bool isSupported )
5791 {
5792   QMutexLocker locker( &mMutex );
5793   mFieldSupportsEnumValues[ index ] = isSupported;
5794 }
5795 
5796 
QgsPostgresProviderMetadata()5797 QgsPostgresProviderMetadata::QgsPostgresProviderMetadata()
5798   : QgsProviderMetadata( QgsPostgresProvider::POSTGRES_KEY, QgsPostgresProvider::POSTGRES_DESCRIPTION )
5799 {
5800 }
5801 
5802 #ifndef HAVE_STATIC_PROVIDERS
providerMetadataFactory()5803 QGISEXTERN QgsProviderMetadata *providerMetadataFactory()
5804 {
5805   return new QgsPostgresProviderMetadata();
5806 }
5807 #endif
5808 
5809 
decodeUri(const QString & uri) const5810 QVariantMap QgsPostgresProviderMetadata::decodeUri( const QString &uri ) const
5811 {
5812   const QgsDataSourceUri dsUri { uri };
5813   QVariantMap uriParts;
5814 
5815   if ( ! dsUri.database().isEmpty() )
5816     uriParts[ QStringLiteral( "dbname" ) ] = dsUri.database();
5817   if ( ! dsUri.host().isEmpty() )
5818     uriParts[ QStringLiteral( "host" ) ] = dsUri.host();
5819   if ( ! dsUri.port().isEmpty() )
5820     uriParts[ QStringLiteral( "port" ) ] = dsUri.port();
5821   if ( ! dsUri.service().isEmpty() )
5822     uriParts[ QStringLiteral( "service" ) ] = dsUri.service();
5823   if ( ! dsUri.username().isEmpty() )
5824     uriParts[ QStringLiteral( "username" ) ] = dsUri.username();
5825   if ( ! dsUri.password().isEmpty() )
5826     uriParts[ QStringLiteral( "password" ) ] = dsUri.password();
5827   if ( ! dsUri.authConfigId().isEmpty() )
5828     uriParts[ QStringLiteral( "authcfg" ) ] = dsUri.authConfigId();
5829   if ( dsUri.wkbType() != QgsWkbTypes::Type::Unknown )
5830     uriParts[ QStringLiteral( "type" ) ] = dsUri.wkbType();
5831 
5832   if ( uri.contains( QStringLiteral( "selectatid=" ), Qt::CaseSensitivity::CaseInsensitive ) )
5833     uriParts[ QStringLiteral( "selectatid" ) ] = ! dsUri.selectAtIdDisabled();
5834 
5835   if ( ! dsUri.table().isEmpty() )
5836     uriParts[ QStringLiteral( "table" ) ] = dsUri.table();
5837   if ( ! dsUri.schema().isEmpty() )
5838     uriParts[ QStringLiteral( "schema" ) ] = dsUri.schema();
5839   if ( ! dsUri.keyColumn().isEmpty() )
5840     uriParts[ QStringLiteral( "key" ) ] = dsUri.keyColumn();
5841   if ( ! dsUri.srid().isEmpty() )
5842     uriParts[ QStringLiteral( "srid" ) ] = dsUri.srid();
5843 
5844   if ( uri.contains( QStringLiteral( "estimatedmetadata=" ), Qt::CaseSensitivity::CaseInsensitive ) )
5845     uriParts[ QStringLiteral( "estimatedmetadata" ) ] = dsUri.useEstimatedMetadata();
5846 
5847   if ( uri.contains( QStringLiteral( "sslmode=" ), Qt::CaseSensitivity::CaseInsensitive ) )
5848     uriParts[ QStringLiteral( "sslmode" ) ] = dsUri.sslMode();
5849 
5850   if ( ! dsUri.sql().isEmpty() )
5851     uriParts[ QStringLiteral( "sql" ) ] = dsUri.sql();
5852   if ( ! dsUri.geometryColumn().isEmpty() )
5853     uriParts[ QStringLiteral( "geometrycolumn" ) ] = dsUri.geometryColumn();
5854 
5855   return uriParts;
5856 }
5857 
5858 
encodeUri(const QVariantMap & parts) const5859 QString QgsPostgresProviderMetadata::encodeUri( const QVariantMap &parts ) const
5860 {
5861   QgsDataSourceUri dsUri;
5862   if ( parts.contains( QStringLiteral( "dbname" ) ) )
5863     dsUri.setDatabase( parts.value( QStringLiteral( "dbname" ) ).toString() );
5864   if ( parts.contains( QStringLiteral( "port" ) ) )
5865     dsUri.setParam( QStringLiteral( "port" ), parts.value( QStringLiteral( "port" ) ).toString() );
5866   if ( parts.contains( QStringLiteral( "host" ) ) )
5867     dsUri.setParam( QStringLiteral( "host" ), parts.value( QStringLiteral( "host" ) ).toString() );
5868   if ( parts.contains( QStringLiteral( "service" ) ) )
5869     dsUri.setParam( QStringLiteral( "service" ), parts.value( QStringLiteral( "service" ) ).toString() );
5870   if ( parts.contains( QStringLiteral( "username" ) ) )
5871     dsUri.setUsername( parts.value( QStringLiteral( "username" ) ).toString() );
5872   if ( parts.contains( QStringLiteral( "password" ) ) )
5873     dsUri.setPassword( parts.value( QStringLiteral( "password" ) ).toString() );
5874   if ( parts.contains( QStringLiteral( "authcfg" ) ) )
5875     dsUri.setAuthConfigId( parts.value( QStringLiteral( "authcfg" ) ).toString() );
5876   if ( parts.contains( QStringLiteral( "type" ) ) )
5877     dsUri.setParam( QStringLiteral( "type" ), QgsWkbTypes::displayString( static_cast<QgsWkbTypes::Type>( parts.value( QStringLiteral( "type" ) ).toInt() ) ) );
5878   if ( parts.contains( QStringLiteral( "selectatid" ) ) )
5879     dsUri.setParam( QStringLiteral( "selectatid" ), parts.value( QStringLiteral( "selectatid" ) ).toString() );
5880   if ( parts.contains( QStringLiteral( "table" ) ) )
5881     dsUri.setTable( parts.value( QStringLiteral( "table" ) ).toString() );
5882   if ( parts.contains( QStringLiteral( "schema" ) ) )
5883     dsUri.setSchema( parts.value( QStringLiteral( "schema" ) ).toString() );
5884   if ( parts.contains( QStringLiteral( "key" ) ) )
5885     dsUri.setParam( QStringLiteral( "key" ), parts.value( QStringLiteral( "key" ) ).toString() );
5886   if ( parts.contains( QStringLiteral( "srid" ) ) )
5887     dsUri.setSrid( parts.value( QStringLiteral( "srid" ) ).toString() );
5888   if ( parts.contains( QStringLiteral( "estimatedmetadata" ) ) )
5889     dsUri.setParam( QStringLiteral( "estimatedmetadata" ), parts.value( QStringLiteral( "estimatedmetadata" ) ).toString() );
5890   if ( parts.contains( QStringLiteral( "sslmode" ) ) )
5891     dsUri.setParam( QStringLiteral( "sslmode" ), QgsDataSourceUri::encodeSslMode( static_cast<QgsDataSourceUri::SslMode>( parts.value( QStringLiteral( "sslmode" ) ).toInt( ) ) ) );
5892   if ( parts.contains( QStringLiteral( "sql" ) ) )
5893     dsUri.setSql( parts.value( QStringLiteral( "sql" ) ).toString() );
5894   if ( parts.contains( QStringLiteral( "checkPrimaryKeyUnicity" ) ) )
5895     dsUri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), parts.value( QStringLiteral( "checkPrimaryKeyUnicity" ) ).toString() );
5896   if ( parts.contains( QStringLiteral( "geometrycolumn" ) ) )
5897     dsUri.setGeometryColumn( parts.value( QStringLiteral( "geometrycolumn" ) ).toString() );
5898   return dsUri.uri( false );
5899 }
5900