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 ¶ms ) 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 "edColNames )
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 ¶ms ) 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