1 /***************************************************************************
2   qgspostgresconn.cpp  -  connection class to PostgreSQL/PostGIS
3                              -------------------
4     begin                : 2011/01/28
5     copyright            : (C) 2011 by Juergen E. Fischer
6     email                : jef at norbit dot de
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 "qgspostgresconn.h"
19 #include "qgsauthmanager.h"
20 #include "qgslogger.h"
21 #include "qgsdatasourceuri.h"
22 #include "qgsmessagelog.h"
23 #include "qgscredentials.h"
24 #include "qgsfields.h"
25 #include "qgspgtablemodel.h"
26 #include "qgsproviderregistry.h"
27 #include "qgsvectordataprovider.h"
28 #include "qgswkbtypes.h"
29 #include "qgssettings.h"
30 #include "qgsjsonutils.h"
31 #include "qgspostgresstringutils.h"
32 #include "qgspostgresconnpool.h"
33 
34 #include <QApplication>
35 #include <QStringList>
36 #include <QThread>
37 
38 #include <climits>
39 
40 #include <nlohmann/json.hpp>
41 
42 // for htonl
43 #ifdef Q_OS_WIN
44 #include <winsock.h>
45 #else
46 #include <netinet/in.h>
47 #endif
48 
49 const int PG_DEFAULT_TIMEOUT = 30;
50 
~QgsPostgresResult()51 QgsPostgresResult::~QgsPostgresResult()
52 {
53   if ( mRes )
54     ::PQclear( mRes );
55   mRes = nullptr;
56 }
57 
operator =(PGresult * result)58 QgsPostgresResult &QgsPostgresResult::operator=( PGresult *result )
59 {
60   if ( mRes )
61     ::PQclear( mRes );
62   mRes = result;
63   return *this;
64 }
65 
operator =(const QgsPostgresResult & src)66 QgsPostgresResult &QgsPostgresResult::operator=( const QgsPostgresResult &src )
67 {
68   if ( mRes )
69     ::PQclear( mRes );
70   mRes = src.result();
71   return *this;
72 }
73 
PQresultStatus()74 ExecStatusType QgsPostgresResult::PQresultStatus()
75 {
76   return mRes ? ::PQresultStatus( mRes ) : PGRES_FATAL_ERROR;
77 }
78 
PQresultErrorMessage()79 QString QgsPostgresResult::PQresultErrorMessage()
80 {
81   return mRes ? QString::fromUtf8( ::PQresultErrorMessage( mRes ) ) : QObject::tr( "no result buffer" );
82 }
83 
PQntuples()84 int QgsPostgresResult::PQntuples()
85 {
86   Q_ASSERT( mRes );
87   return ::PQntuples( mRes );
88 }
89 
PQgetvalue(int row,int col)90 QString QgsPostgresResult::PQgetvalue( int row, int col )
91 {
92   Q_ASSERT( mRes );
93   return PQgetisnull( row, col )
94          ? QString()
95          : QString::fromUtf8( ::PQgetvalue( mRes, row, col ) );
96 }
97 
PQgetisnull(int row,int col)98 bool QgsPostgresResult::PQgetisnull( int row, int col )
99 {
100   Q_ASSERT( mRes );
101   return ::PQgetisnull( mRes, row, col );
102 }
103 
PQnfields()104 int QgsPostgresResult::PQnfields()
105 {
106   Q_ASSERT( mRes );
107   return ::PQnfields( mRes );
108 }
109 
PQfname(int col)110 QString QgsPostgresResult::PQfname( int col )
111 {
112   Q_ASSERT( mRes );
113   return QString::fromUtf8( ::PQfname( mRes, col ) );
114 }
115 
PQftable(int col)116 Oid QgsPostgresResult::PQftable( int col )
117 {
118   Q_ASSERT( mRes );
119   return ::PQftable( mRes, col );
120 }
121 
PQftablecol(int col)122 int QgsPostgresResult::PQftablecol( int col )
123 {
124   Q_ASSERT( mRes );
125   return ::PQftablecol( mRes, col );
126 }
127 
PQftype(int col)128 Oid QgsPostgresResult::PQftype( int col )
129 {
130   Q_ASSERT( mRes );
131   return ::PQftype( mRes, col );
132 }
133 
PQfmod(int col)134 int QgsPostgresResult::PQfmod( int col )
135 {
136   Q_ASSERT( mRes );
137   return ::PQfmod( mRes, col );
138 }
139 
PQoidValue()140 Oid QgsPostgresResult::PQoidValue()
141 {
142   Q_ASSERT( mRes );
143   return ::PQoidValue( mRes );
144 }
145 
QgsPoolPostgresConn(const QString & connInfo)146 QgsPoolPostgresConn::QgsPoolPostgresConn( const QString &connInfo )
147   : mPgConn( QgsPostgresConnPool::instance()->acquireConnection( connInfo ) )
148 {
149 }
150 
~QgsPoolPostgresConn()151 QgsPoolPostgresConn::~QgsPoolPostgresConn()
152 {
153   if ( mPgConn )
154     QgsPostgresConnPool::instance()->releaseConnection( mPgConn );
155 }
156 
157 
158 QMap<QString, QgsPostgresConn *> QgsPostgresConn::sConnectionsRO;
159 QMap<QString, QgsPostgresConn *> QgsPostgresConn::sConnectionsRW;
160 
161 const int QgsPostgresConn::GEOM_TYPE_SELECT_LIMIT = 100;
162 
connectDb(const QString & conninfo,bool readonly,bool shared,bool transaction)163 QgsPostgresConn *QgsPostgresConn::connectDb( const QString &conninfo, bool readonly, bool shared, bool transaction )
164 {
165   QMap<QString, QgsPostgresConn *> &connections =
166     readonly ? QgsPostgresConn::sConnectionsRO : QgsPostgresConn::sConnectionsRW;
167 
168   // This is called from may places where shared parameter cannot be forced to false (QgsVectorLayerExporter)
169   // and which is run in a different thread (drag and drop in browser)
170   if ( QApplication::instance()->thread() != QThread::currentThread() )
171   {
172     shared = false;
173   }
174 
175   if ( shared )
176   {
177     // sharing connection between threads is not safe
178     // See https://github.com/qgis/QGIS/issues/21205
179     Q_ASSERT( QApplication::instance()->thread() == QThread::currentThread() );
180 
181     if ( connections.contains( conninfo ) )
182     {
183       QgsDebugMsgLevel( QStringLiteral( "Using cached connection for %1" ).arg( conninfo ), 2 );
184       connections[conninfo]->mRef++;
185       return connections[conninfo];
186     }
187   }
188 
189   QgsPostgresConn *conn = new QgsPostgresConn( conninfo, readonly, shared, transaction );
190 
191   if ( conn->mRef == 0 )
192   {
193     delete conn;
194     return nullptr;
195   }
196 
197   if ( shared )
198   {
199     connections.insert( conninfo, conn );
200   }
201 
202   return conn;
203 }
204 
noticeProcessor(void * arg,const char * message)205 static void noticeProcessor( void *arg, const char *message )
206 {
207   Q_UNUSED( arg )
208   QString msg( QString::fromUtf8( message ) );
209   msg.chop( 1 );
210   QgsMessageLog::logMessage( QObject::tr( "NOTICE: %1" ).arg( msg ), QObject::tr( "PostGIS" ) );
211 }
212 
QgsPostgresConn(const QString & conninfo,bool readOnly,bool shared,bool transaction)213 QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool shared, bool transaction )
214   : mRef( 1 )
215   , mOpenCursors( 0 )
216   , mConnInfo( conninfo )
217   , mGeosAvailable( false )
218   , mProjAvailable( false )
219   , mTopologyAvailable( false )
220   , mGotPostgisVersion( false )
221   , mPostgresqlVersion( 0 )
222   , mPostgisVersionMajor( 0 )
223   , mPostgisVersionMinor( 0 )
224   , mPointcloudAvailable( false )
225   , mRasterAvailable( false )
226   , mUseWkbHex( false )
227   , mReadOnly( readOnly )
228   , mSwapEndian( false )
229   , mNextCursorId( 0 )
230   , mShared( shared )
231   , mTransaction( transaction )
232 {
233 
234   QgsDebugMsgLevel( QStringLiteral( "New PostgreSQL connection for " ) + conninfo, 2 );
235 
236   // expand connectionInfo
237   QgsDataSourceUri uri( conninfo );
238   QString expandedConnectionInfo = uri.connectionInfo( true );
239 
240   auto addDefaultTimeoutAndClientEncoding = []( QString & connectString )
241   {
242     if ( !connectString.contains( QStringLiteral( "connect_timeout=" ) ) )
243     {
244       // add default timeout
245       QgsSettings settings;
246       int timeout = settings.value( QStringLiteral( "PostgreSQL/default_timeout" ), PG_DEFAULT_TIMEOUT, QgsSettings::Providers ).toInt();
247       connectString += QStringLiteral( " connect_timeout=%1" ).arg( timeout );
248     }
249 
250     connectString += QLatin1String( " client_encoding='UTF-8'" );
251   };
252   addDefaultTimeoutAndClientEncoding( expandedConnectionInfo );
253 
254   mConn = PQconnectdb( expandedConnectionInfo.toUtf8() );
255 
256   // remove temporary cert/key/CA if they exist
257   QgsDataSourceUri expandedUri( expandedConnectionInfo );
258   QStringList parameters;
259   parameters << QStringLiteral( "sslcert" ) << QStringLiteral( "sslkey" ) << QStringLiteral( "sslrootcert" );
260   const auto constParameters = parameters;
261   for ( const QString &param : constParameters )
262   {
263     if ( expandedUri.hasParam( param ) )
264     {
265       QString fileName = expandedUri.param( param );
266       fileName.remove( QStringLiteral( "'" ) );
267       QFile file( fileName );
268       // set minimal permission to allow removing on Win.
269       // On linux and Mac if file is set with QFile::ReadUser
270       // does not create problem removing certs
271       if ( !file.setPermissions( QFile::WriteOwner ) )
272       {
273         QString errorMsg = tr( "Cannot set WriteOwner permission to cert: %0 to allow removing it" ).arg( file.fileName() );
274         PQfinish();
275         QgsMessageLog::logMessage( tr( "Client security failure" ) + '\n' + errorMsg, tr( "PostGIS" ) );
276         mRef = 0;
277         return;
278       }
279       if ( !file.remove() )
280       {
281         QString errorMsg = tr( "Cannot remove cert: %0" ).arg( file.fileName() );
282         PQfinish();
283         QgsMessageLog::logMessage( tr( "Client security failure" ) + '\n' + errorMsg, tr( "PostGIS" ) );
284         mRef = 0;
285         return;
286       }
287     }
288   }
289 
290   // check the connection status
291   if ( PQstatus() != CONNECTION_OK )
292   {
293     QString username = uri.username();
294     QString password = uri.password();
295 
296     QgsCredentials::instance()->lock();
297 
298     int i = 0;
299     while ( PQstatus() != CONNECTION_OK && i < 5 )
300     {
301       ++i;
302       bool ok = QgsCredentials::instance()->get( conninfo, username, password, PQerrorMessage() );
303       if ( !ok )
304       {
305         break;
306       }
307 
308       PQfinish();
309 
310       if ( !username.isEmpty() )
311         uri.setUsername( username );
312 
313       if ( !password.isEmpty() )
314         uri.setPassword( password );
315 
316       QgsDebugMsgLevel( "Connecting to " + uri.connectionInfo( false ), 2 );
317       QString connectString = uri.connectionInfo();
318       addDefaultTimeoutAndClientEncoding( connectString );
319       mConn = PQconnectdb( connectString.toUtf8() );
320     }
321 
322     if ( PQstatus() == CONNECTION_OK )
323       QgsCredentials::instance()->put( conninfo, username, password );
324 
325     QgsCredentials::instance()->unlock();
326   }
327 
328   if ( PQstatus() != CONNECTION_OK )
329   {
330     QString errorMsg = PQerrorMessage();
331     PQfinish();
332     QgsMessageLog::logMessage( tr( "Connection to database failed" ) + '\n' + errorMsg, tr( "PostGIS" ) );
333     mRef = 0;
334     return;
335   }
336 
337   QgsDebugMsgLevel( QStringLiteral( "Connection to the database was successful" ), 2 );
338 
339   deduceEndian();
340 
341   /* Check to see if we have working PostGIS support */
342   if ( !postgisVersion().isNull() )
343   {
344     /* Check to see if we have GEOS support and if not, warn the user about
345        the problems they will see :) */
346     QgsDebugMsgLevel( QStringLiteral( "Checking for GEOS support" ), 3 );
347 
348     if ( !hasGEOS() )
349     {
350       QgsMessageLog::logMessage( tr( "Your PostGIS installation has no GEOS support. Feature selection and identification will not work properly. Please install PostGIS with GEOS support (http://geos.refractions.net)" ), tr( "PostGIS" ) );
351     }
352     else
353     {
354       QgsDebugMsgLevel( QStringLiteral( "GEOS support available!" ), 3 );
355     }
356   }
357 
358   if ( mPostgresqlVersion >= 90000 )
359   {
360     PQexecNR( QStringLiteral( "SET application_name='QGIS'" ) );
361     PQexecNR( QStringLiteral( "SET extra_float_digits=3" ) );
362   }
363 
364   PQsetNoticeProcessor( mConn, noticeProcessor, nullptr );
365 }
366 
~QgsPostgresConn()367 QgsPostgresConn::~QgsPostgresConn()
368 {
369   Q_ASSERT( mRef == 0 );
370   if ( mConn )
371     ::PQfinish( mConn );
372   mConn = nullptr;
373 }
374 
ref()375 void QgsPostgresConn::ref()
376 {
377   QMutexLocker locker( &mLock );
378   ++mRef;
379 }
380 
unref()381 void QgsPostgresConn::unref()
382 {
383   QMutexLocker locker( &mLock );
384   if ( --mRef > 0 )
385     return;
386 
387   if ( mShared )
388   {
389     QMap<QString, QgsPostgresConn *> &connections = mReadOnly ? sConnectionsRO : sConnectionsRW;
390 
391     QString key = connections.key( this, QString() );
392 
393     Q_ASSERT( !key.isNull() );
394     connections.remove( key );
395   }
396 
397   // to avoid destroying locked mutex
398   locker.unlock();
399   delete this;
400 }
401 
402 /* private */
supportedSpatialTypes() const403 QStringList QgsPostgresConn::supportedSpatialTypes() const
404 {
405   QStringList supportedSpatialTypes;
406 
407   supportedSpatialTypes << quotedValue( "geometry" )
408                         << quotedValue( "geography" );
409 
410   if ( hasPointcloud() )
411   {
412     supportedSpatialTypes << quotedValue( "pcpatch" );
413     supportedSpatialTypes << quotedValue( "pcpoint" );
414   }
415 
416   if ( hasRaster() )
417     supportedSpatialTypes << quotedValue( "raster" );
418 
419   if ( hasTopology() )
420     supportedSpatialTypes << quotedValue( "topogeometry" );
421 
422   return supportedSpatialTypes;
423 }
424 
425 /* private */
426 // TODO: deprecate this function
addColumnInfo(QgsPostgresLayerProperty & layerProperty,const QString & schemaName,const QString & viewName,bool fetchPkCandidates)427 void QgsPostgresConn::addColumnInfo( QgsPostgresLayerProperty &layerProperty, const QString &schemaName, const QString &viewName, bool fetchPkCandidates )
428 {
429   // TODO: optimize this query when pk candidates aren't needed
430   //       could use array_agg() and count()
431   //       array output would look like this: "{One,tWo}"
432   QString sql = QStringLiteral( "SELECT attname, CASE WHEN typname in (%1) THEN 1 ELSE null END AS isSpatial FROM pg_attribute JOIN pg_type ON atttypid=pg_type.oid WHERE attrelid=regclass('%2.%3') AND NOT attisdropped AND attnum>0 ORDER BY attnum" )
433                 .arg( supportedSpatialTypes().join( ',' ) )
434                 .arg( quotedIdentifier( schemaName ),
435                       quotedIdentifier( viewName ) );
436   QgsDebugMsgLevel( "getting column info: " + sql, 2 );
437   QgsPostgresResult colRes( PQexec( sql ) );
438 
439   layerProperty.pkCols.clear();
440   layerProperty.nSpCols = 0;
441 
442   if ( colRes.PQresultStatus() == PGRES_TUPLES_OK )
443   {
444     for ( int i = 0; i < colRes.PQntuples(); i++ )
445     {
446       if ( fetchPkCandidates )
447       {
448         layerProperty.pkCols << colRes.PQgetvalue( i, 0 );
449       }
450 
451       if ( colRes.PQgetisnull( i, 1 ) == 0 )
452       {
453         ++layerProperty.nSpCols;
454       }
455     }
456   }
457   else
458   {
459     QgsMessageLog::logMessage( tr( "SQL: %1\nresult: %2\nerror: %3\n" ).arg( sql ).arg( colRes.PQresultStatus() ).arg( colRes.PQresultErrorMessage() ), tr( "PostGIS" ) );
460   }
461 
462 }
463 
getTableInfo(bool searchGeometryColumnsOnly,bool searchPublicOnly,bool allowGeometrylessTables,const QString & schema)464 bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchPublicOnly, bool allowGeometrylessTables, const QString &schema )
465 {
466   QMutexLocker locker( &mLock );
467   int nColumns = 0;
468   int foundInTables = 0;
469   QgsPostgresResult result;
470   QString query;
471 
472   mLayersSupported.clear();
473 
474   for ( int i = SctGeometry; i <= SctRaster; ++i )
475   {
476     QString sql, tableName, schemaName, columnName, typeName, sridName, gtableName, dimName;
477 
478     if ( i == SctGeometry )
479     {
480       tableName  = QStringLiteral( "l.f_table_name" );
481       schemaName = QStringLiteral( "l.f_table_schema" );
482       columnName = QStringLiteral( "l.f_geometry_column" );
483       typeName   = QStringLiteral( "upper(l.type)" );
484       sridName   = QStringLiteral( "l.srid" );
485       dimName    = QStringLiteral( "l.coord_dimension" );
486       gtableName = QStringLiteral( "geometry_columns" );
487     }
488     // Geography since postgis 1.5
489     else if ( i == SctGeography
490               && ( mPostgisVersionMajor >= 2
491                    || ( mPostgisVersionMajor == 1 && mPostgisVersionMinor >= 5 ) ) )
492     {
493       tableName  = QStringLiteral( "l.f_table_name" );
494       schemaName = QStringLiteral( "l.f_table_schema" );
495       columnName = QStringLiteral( "l.f_geography_column" );
496       typeName   = QStringLiteral( "upper(l.type)" );
497       sridName   = QStringLiteral( "l.srid" );
498       dimName    = QStringLiteral( "2" );
499       gtableName = QStringLiteral( "geography_columns" );
500     }
501     else if ( i == SctTopoGeometry )
502     {
503       if ( !hasTopology() )
504         continue;
505 
506       schemaName = QStringLiteral( "l.schema_name" );
507       tableName  = QStringLiteral( "l.table_name" );
508       columnName = QStringLiteral( "l.feature_column" );
509       typeName   = "CASE "
510                    "WHEN l.feature_type = 1 THEN 'MULTIPOINT' "
511                    "WHEN l.feature_type = 2 THEN 'MULTILINESTRING' "
512                    "WHEN l.feature_type = 3 THEN 'MULTIPOLYGON' "
513                    "WHEN l.feature_type = 4 THEN 'GEOMETRYCOLLECTION' "
514                    "END AS type";
515       sridName   = QStringLiteral( "(SELECT srid FROM topology.topology t WHERE l.topology_id=t.id)" );
516       dimName    = QStringLiteral( "2" );
517       gtableName = QStringLiteral( "topology.layer" );
518     }
519     else if ( i == SctPcPatch )
520     {
521       if ( !hasPointcloud() )
522         continue;
523 
524       tableName  = QStringLiteral( "l.\"table\"" );
525       schemaName = QStringLiteral( "l.\"schema\"" );
526       columnName = QStringLiteral( "l.\"column\"" );
527       typeName   = QStringLiteral( "'POLYGON'" );
528       sridName   = QStringLiteral( "l.srid" );
529       dimName    = QStringLiteral( "2" );
530       gtableName = QStringLiteral( "pointcloud_columns" );
531     }
532     else if ( i == SctRaster )
533     {
534       if ( !hasRaster() )
535         continue;
536 
537       tableName  = QStringLiteral( "l.\"r_table_name\"" );
538       schemaName = QStringLiteral( "l.\"r_table_schema\"" );
539       columnName = QStringLiteral( "l.\"r_raster_column\"" );
540       typeName   = QStringLiteral( "'RASTER'" );
541       sridName   = QStringLiteral( "l.srid" );
542       dimName    = QStringLiteral( "2" );
543       gtableName = QStringLiteral( "raster_columns" );
544     }
545     else
546     {
547       QgsMessageLog::logMessage( tr( "Unsupported spatial column type %1" )
548                                  .arg( displayStringForGeomType( ( QgsPostgresGeometryColumnType )i ) ) );
549       continue;
550     }
551 
552     // The following query returns only tables that exist and the user has SELECT privilege on.
553     // Can't use regclass here because table must exist, else error occurs.
554     sql = QString( "SELECT %1,%2,%3,%4,%5,%6,c.relkind,obj_description(c.oid),"
555                    "%10, "
556                    "count(CASE WHEN t.typname IN (%9) THEN 1 ELSE NULL END) "
557                    ", %8 "
558                    " FROM %7 l,pg_class c,pg_namespace n,pg_attribute a,pg_type t"
559                    " WHERE c.relname=%1"
560                    " AND %2=n.nspname"
561                    " AND NOT a.attisdropped"
562                    " AND a.attrelid=c.oid"
563                    " AND a.atttypid=t.oid"
564                    " AND a.attnum>0"
565                    " AND n.oid=c.relnamespace"
566                    " AND has_schema_privilege(n.nspname,'usage')"
567                    " AND has_table_privilege(c.oid,'select')" // user has select privilege
568                  )
569           .arg( tableName, schemaName, columnName, typeName, sridName, dimName, gtableName )
570           .arg( i )
571           .arg( supportedSpatialTypes().join( ',' ) )
572           .arg( mPostgresqlVersion >= 90000 ? "array_agg(a.attname ORDER BY a.attnum)" : "(SELECT array_agg(attname) FROM (SELECT unnest(array_agg(a.attname)) AS attname ORDER BY unnest(array_agg(a.attnum))) AS attname)" )
573           ;
574 
575     if ( searchPublicOnly )
576       sql += QLatin1String( " AND n.nspname='public'" );
577 
578     if ( !schema.isEmpty() )
579       sql += QStringLiteral( " AND %1='%2'" ).arg( schemaName, schema );
580 
581     sql += QString( " GROUP BY 1,2,3,4,5,6,7,c.oid,11" );
582 
583     foundInTables |= 1 << i;
584 
585     if ( ! query.isEmpty() )
586       query += " UNION ";
587 
588     query += sql;
589   }
590 
591   query += QLatin1String( " ORDER BY 2,1,3" );
592 
593 
594   QgsDebugMsgLevel( "getting table info from layer registries: " + query, 2 );
595   result = PQexec( query, true );
596   // NOTE: we intentionally continue if the query fails
597   //       (for example because PostGIS is not installed)
598   for ( int idx = 0; idx < result.PQntuples(); idx++ )
599   {
600     QString tableName = result.PQgetvalue( idx, 0 );
601     QString schemaName = result.PQgetvalue( idx, 1 );
602     QString column = result.PQgetvalue( idx, 2 );
603     QString type = result.PQgetvalue( idx, 3 );
604     QString ssrid = result.PQgetvalue( idx, 4 );
605     int dim = result.PQgetvalue( idx, 5 ).toInt();
606     QString relkind = result.PQgetvalue( idx, 6 );
607     bool isView = relkind == QLatin1String( "v" ) || relkind == QLatin1String( "m" );
608     bool isMaterializedView = relkind == QLatin1String( "m" );
609     bool isForeignTable = relkind == QLatin1String( "f" );
610     bool isRaster = type == QLatin1String( "RASTER" );
611     QString comment = result.PQgetvalue( idx, 7 );
612     QString attributes = result.PQgetvalue( idx, 8 );
613     int nSpCols = result.PQgetvalue( idx, 9 ).toInt();
614     QgsPostgresGeometryColumnType columnType = SctNone;
615 
616     int columnTypeInt = result.PQgetvalue( idx, 10 ).toInt();
617     if ( columnTypeInt == SctGeometry )
618       columnType = SctGeometry;
619     else if ( columnTypeInt == SctGeography )
620       columnType = SctGeography;
621     else if ( columnTypeInt == SctTopoGeometry )
622       columnType = SctTopoGeometry;
623     else if ( columnTypeInt == SctPcPatch )
624       columnType = SctPcPatch;
625     else if ( columnTypeInt == SctRaster )
626       columnType = SctRaster;
627     else
628     {
629       QgsDebugMsg( QStringLiteral( "Unhandled columnType index %1" )
630                    .  arg( columnTypeInt ) );
631     }
632 
633     int srid = ssrid.isEmpty() ? std::numeric_limits<int>::min() : ssrid.toInt();
634 
635     if ( ! isRaster && majorVersion() >= 2 && srid == 0 )
636     {
637       // 0 doesn't constraint => detect
638       srid = std::numeric_limits<int>::min();
639     }
640 
641 #if 0
642     QgsDebugMsgLevel( QStringLiteral( "%1 : %2.%3.%4: %5 %6 %7 %8" )
643                       .arg( gtableName )
644                       .arg( schemaName ).arg( tableName ).arg( column )
645                       .arg( type )
646                       .arg( srid )
647                       .arg( relkind )
648                       .arg( dim ), 2 );
649 #endif
650 
651     QgsPostgresLayerProperty layerProperty;
652     layerProperty.schemaName = schemaName;
653     layerProperty.tableName = tableName;
654     layerProperty.geometryColName = column;
655     layerProperty.geometryColType = columnType;
656     if ( dim == 3 && !type.endsWith( 'M' ) )
657       type += QLatin1Char( 'Z' );
658     else if ( dim == 4 )
659       type += QLatin1String( "ZM" );
660     layerProperty.types = QList<QgsWkbTypes::Type>() << ( QgsPostgresConn::wkbTypeFromPostgis( type ) );
661     layerProperty.srids = QList<int>() << srid;
662     layerProperty.sql.clear();
663     layerProperty.relKind = relkind;
664     layerProperty.isView = isView;
665     layerProperty.isForeignTable = isForeignTable;
666     layerProperty.isRaster = isRaster;
667     layerProperty.isMaterializedView = isMaterializedView;
668     layerProperty.tableComment = comment;
669     layerProperty.nSpCols = nSpCols;
670     if ( isView || isForeignTable )
671     {
672       // TODO: use std::transform
673       for ( const auto &a : QgsPostgresStringUtils::parseArray( attributes ) )
674       {
675         layerProperty.pkCols << a.toString();
676       }
677     }
678 
679     if ( isView && layerProperty.pkCols.empty() )
680     {
681       //QgsDebugMsgLevel( QStringLiteral( "no key columns found." ), 2 );
682       continue;
683     }
684 
685     mLayersSupported << layerProperty;
686     nColumns++;
687   }
688 
689   //search for geometry columns in tables that are not in the geometry_columns metatable
690   if ( !searchGeometryColumnsOnly )
691   {
692     // Now have a look for spatial columns that aren't in the geometry_columns table.
693     QString sql = QStringLiteral( "SELECT"
694                                   " c.relname"
695                                   ",n.nspname"
696                                   ",a.attname"
697                                   ",c.relkind"
698                                   ",CASE WHEN t.typname IN (%1) THEN t.typname ELSE b.typname END AS coltype"
699                                   ",obj_description(c.oid)"
700                                   " FROM pg_attribute a"
701                                   " JOIN pg_class c ON c.oid=a.attrelid"
702                                   " JOIN pg_namespace n ON n.oid=c.relnamespace"
703                                   " JOIN pg_type t ON t.oid=a.atttypid"
704                                   " LEFT JOIN pg_type b ON b.oid=t.typbasetype"
705                                   " WHERE c.relkind IN ('v','r','m','p','f')"
706                                   " AND has_schema_privilege( n.nspname, 'usage' )"
707                                   " AND has_table_privilege( c.oid, 'select' )"
708                                   " AND (t.typname IN (%1) OR b.typname IN (%1))" )
709                   .arg( supportedSpatialTypes().join( ',' ) );
710 
711     // user has select privilege
712     if ( searchPublicOnly )
713       sql += QLatin1String( " AND n.nspname='public'" );
714 
715     if ( !schema.isEmpty() )
716       sql += QStringLiteral( " AND n.nspname='%2'" ).arg( schema );
717 
718     // skip columns of which we already derived information from the metadata tables
719     if ( nColumns > 0 )
720     {
721       if ( foundInTables & ( 1 << SctGeometry ) )
722       {
723         sql += QLatin1String( " AND NOT EXISTS (SELECT 1 FROM geometry_columns WHERE n.nspname=f_table_schema AND c.relname=f_table_name AND a.attname=f_geometry_column)" );
724       }
725 
726       if ( foundInTables & ( 1 << SctGeography ) )
727       {
728         sql += QLatin1String( " AND NOT EXISTS (SELECT 1 FROM geography_columns WHERE n.nspname=f_table_schema AND c.relname=f_table_name AND a.attname=f_geography_column)" );
729       }
730 
731       if ( foundInTables & ( 1 << SctPcPatch ) )
732       {
733         sql += QLatin1String( " AND NOT EXISTS (SELECT 1 FROM pointcloud_columns WHERE n.nspname=\"schema\" AND c.relname=\"table\" AND a.attname=\"column\")" );
734       }
735 
736       if ( foundInTables & ( 1 << SctRaster ) )
737       {
738         sql += QLatin1String( " AND NOT EXISTS (SELECT 1 FROM raster_columns WHERE n.nspname=\"r_table_schema\" AND c.relname=\"r_table_name\" AND a.attname=\"r_raster_column\")" );
739       }
740 
741       if ( foundInTables & ( 1 << SctTopoGeometry ) )
742       {
743         sql += QLatin1String( " AND NOT EXISTS (SELECT 1 FROM topology.layer WHERE n.nspname=\"schema_name\" AND c.relname=\"table_name\" AND a.attname=\"feature_column\")" );
744       }
745     }
746 
747     QgsDebugMsgLevel( "getting spatial table info from pg_catalog: " + sql, 2 );
748 
749     result = PQexec( sql );
750 
751     if ( result.PQresultStatus() != PGRES_TUPLES_OK )
752     {
753       QgsMessageLog::logMessage( tr( "Database connection was successful, but the accessible tables could not be determined. The error message from the database was:\n%1\n" )
754                                  .arg( result.PQresultErrorMessage() ),
755                                  tr( "PostGIS" ) );
756       PQexecNR( QStringLiteral( "COMMIT" ) );
757       return false;
758     }
759 
760     for ( int i = 0; i < result.PQntuples(); i++ )
761     {
762       // Have the column name, schema name and the table name. The concept of a
763       // catalog doesn't exist in PostgreSQL so we ignore that, but we
764       // do need to get the geometry type.
765 
766       QString tableName  = result.PQgetvalue( i, 0 ); // relname
767       QString schemaName = result.PQgetvalue( i, 1 ); // nspname
768       QString column     = result.PQgetvalue( i, 2 ); // attname
769       QString relkind    = result.PQgetvalue( i, 3 ); // relation kind
770       QString coltype    = result.PQgetvalue( i, 4 ); // column type
771       bool isView = relkind == QLatin1String( "v" ) || relkind == QLatin1String( "m" );
772       bool isMaterializedView = relkind == QLatin1String( "m" );
773       bool isForeignTable = relkind == QLatin1String( "f" );
774       QString comment    = result.PQgetvalue( i, 5 ); // table comment
775 
776       QgsPostgresLayerProperty layerProperty;
777       layerProperty.types = QList<QgsWkbTypes::Type>() << QgsWkbTypes::Unknown;
778       layerProperty.srids = QList<int>() << std::numeric_limits<int>::min();
779       layerProperty.schemaName = schemaName;
780       layerProperty.tableName = tableName;
781       layerProperty.geometryColName = column;
782       layerProperty.relKind = relkind;
783       layerProperty.isView = isView;
784       layerProperty.isForeignTable = isForeignTable;
785       layerProperty.isRaster = coltype == QLatin1String( "raster" );
786       layerProperty.isMaterializedView = isMaterializedView;
787       layerProperty.tableComment = comment;
788       if ( coltype == QLatin1String( "geometry" ) )
789       {
790         layerProperty.geometryColType = SctGeometry;
791       }
792       else if ( coltype == QLatin1String( "geography" ) )
793       {
794         layerProperty.geometryColType = SctGeography;
795       }
796       else if ( coltype == QLatin1String( "topogeometry" ) )
797       {
798         layerProperty.geometryColType = SctTopoGeometry;
799       }
800       else if ( coltype == QLatin1String( "pcpatch" ) ||
801                 coltype == QLatin1String( "pcpoint" ) )
802       {
803         layerProperty.geometryColType = SctPcPatch;
804       }
805       else if ( coltype == QLatin1String( "raster" ) )
806       {
807         layerProperty.geometryColType = SctRaster;
808       }
809       else
810       {
811         Q_ASSERT( !"Unknown geometry type" );
812       }
813 
814       // TODO: use knowledge from already executed query to count
815       //       spatial fields and list attribute names...
816       addColumnInfo( layerProperty, schemaName, tableName, isView || isForeignTable );
817 
818       if ( isView && layerProperty.pkCols.empty() )
819       {
820         //QgsDebugMsgLevel( QStringLiteral( "no key columns found." ), 2 );
821         continue;
822       }
823 
824       mLayersSupported << layerProperty;
825       nColumns++;
826     }
827   }
828 
829   if ( allowGeometrylessTables )
830   {
831     QString sql = QStringLiteral( "SELECT "
832                                   "pg_class.relname"
833                                   ",pg_namespace.nspname"
834                                   ",pg_class.relkind"
835                                   ",obj_description(pg_class.oid)"
836                                   ",%1"
837                                   " FROM "
838                                   " pg_class"
839                                   ",pg_namespace"
840                                   ",pg_attribute a"
841                                   " WHERE pg_namespace.oid=pg_class.relnamespace"
842                                   " AND has_schema_privilege(pg_namespace.nspname,'usage')"
843                                   " AND has_table_privilege(pg_class.oid,'select')"
844                                   " AND pg_class.relkind IN ('v','r','m','p','f')"
845                                   " AND pg_class.oid = a.attrelid"
846                                   " AND NOT a.attisdropped"
847                                   " AND a.attnum > 0" )
848                   .arg( mPostgresqlVersion >= 90000 ? "array_agg(a.attname ORDER BY a.attnum)" : "(SELECT array_agg(attname) FROM (SELECT unnest(array_agg(a.attname)) AS attname ORDER BY unnest(array_agg(a.attnum))) AS attname)" );
849 
850     // user has select privilege
851     if ( searchPublicOnly )
852       sql += QLatin1String( " AND pg_namespace.nspname='public'" );
853 
854     if ( !schema.isEmpty() )
855       sql += QStringLiteral( " AND pg_namespace.nspname='%2'" ).arg( schema );
856 
857     sql += QLatin1String( " GROUP BY 1,2,3,4" );
858 
859     QgsDebugMsgLevel( "getting non-spatial table info: " + sql, 2 );
860 
861     result = PQexec( sql );
862 
863     if ( result.PQresultStatus() != PGRES_TUPLES_OK )
864     {
865       QgsMessageLog::logMessage( tr( "Database connection was successful, but the accessible tables could not be determined.\nThe error message from the database was:\n%1" )
866                                  .arg( result.PQresultErrorMessage() ),
867                                  tr( "PostGIS" ) );
868       return false;
869     }
870 
871     for ( int i = 0; i < result.PQntuples(); i++ )
872     {
873       QString table   = result.PQgetvalue( i, 0 ); // relname
874       QString schema  = result.PQgetvalue( i, 1 ); // nspname
875       QString relkind = result.PQgetvalue( i, 2 ); // relation kind
876       QString comment = result.PQgetvalue( i, 3 ); // table comment
877       QString attributes = result.PQgetvalue( i, 4 ); // attributes array
878       bool isView = relkind == QLatin1String( "v" ) || relkind == QLatin1String( "m" );
879       bool isMaterializedView = relkind == QLatin1String( "m" );
880       bool isForeignTable = relkind == QLatin1String( "f" );
881 
882       QgsPostgresLayerProperty layerProperty;
883       layerProperty.types = QList<QgsWkbTypes::Type>() << QgsWkbTypes::NoGeometry;
884       layerProperty.srids = QList<int>() << std::numeric_limits<int>::min();
885       layerProperty.schemaName = schema;
886       layerProperty.tableName = table;
887       layerProperty.geometryColName = QString();
888       layerProperty.geometryColType = SctNone;
889       layerProperty.nSpCols = 0;
890       layerProperty.relKind = relkind;
891       layerProperty.isView = isView;
892       layerProperty.isForeignTable = isForeignTable;
893       layerProperty.isRaster = false;
894       layerProperty.isMaterializedView = isMaterializedView;
895       layerProperty.tableComment = comment;
896 
897       //check if we've already added this layer in some form
898       bool alreadyFound = false;
899       const auto constMLayersSupported = mLayersSupported;
900       for ( const QgsPostgresLayerProperty &foundLayer : constMLayersSupported )
901       {
902         if ( foundLayer.schemaName == schema && foundLayer.tableName == table )
903         {
904           //already found this table
905           alreadyFound = true;
906           break;
907         }
908       }
909       if ( alreadyFound )
910         continue;
911 
912       if ( isView || isForeignTable )
913       {
914         // TODO: use std::transform
915         for ( const auto &a : QgsPostgresStringUtils::parseArray( attributes ) )
916         {
917           layerProperty.pkCols << a.toString();
918         }
919       }
920 
921       mLayersSupported << layerProperty;
922       nColumns++;
923     }
924   }
925 
926   if ( nColumns == 0 && schema.isEmpty() )
927   {
928     QgsMessageLog::logMessage( tr( "Database connection was successful, but the accessible tables could not be determined." ), tr( "PostGIS" ) );
929   }
930 
931   return true;
932 }
933 
supportedLayers(QVector<QgsPostgresLayerProperty> & layers,bool searchGeometryColumnsOnly,bool searchPublicOnly,bool allowGeometrylessTables,const QString & schema)934 bool QgsPostgresConn::supportedLayers( QVector<QgsPostgresLayerProperty> &layers, bool searchGeometryColumnsOnly, bool searchPublicOnly, bool allowGeometrylessTables, const QString &schema )
935 {
936   QMutexLocker locker( &mLock );
937 
938   // Get the list of supported tables
939   if ( !getTableInfo( searchGeometryColumnsOnly, searchPublicOnly, allowGeometrylessTables, schema ) )
940   {
941     QgsMessageLog::logMessage( tr( "Unable to get list of spatially enabled tables from the database" ), tr( "PostGIS" ) );
942     return false;
943   }
944 
945   layers = mLayersSupported;
946 
947   return true;
948 }
949 
getSchemas(QList<QgsPostgresSchemaProperty> & schemas)950 bool QgsPostgresConn::getSchemas( QList<QgsPostgresSchemaProperty> &schemas )
951 {
952   schemas.clear();
953   QgsPostgresResult result;
954 
955   QString sql = QStringLiteral( "SELECT nspname, pg_get_userbyid(nspowner), pg_catalog.obj_description(oid) FROM pg_namespace WHERE nspname !~ '^pg_' AND nspname != 'information_schema' ORDER BY nspname" );
956 
957   result = PQexec( sql, true );
958   if ( result.PQresultStatus() != PGRES_TUPLES_OK )
959   {
960     PQexecNR( QStringLiteral( "COMMIT" ) );
961     return false;
962   }
963 
964   for ( int idx = 0; idx < result.PQntuples(); idx++ )
965   {
966     QgsPostgresSchemaProperty schema;
967     schema.name = result.PQgetvalue( idx, 0 );
968     schema.owner = result.PQgetvalue( idx, 1 );
969     schema.description = result.PQgetvalue( idx, 2 );
970     schemas << schema;
971   }
972   return true;
973 }
974 
975 /**
976  * Check to see if GEOS is available
977  */
hasGEOS() const978 bool QgsPostgresConn::hasGEOS() const
979 {
980   // make sure info is up to date for the current connection
981   postgisVersion();
982   return mGeosAvailable;
983 }
984 
985 /**
986  * Check to see if topology is available
987  */
hasTopology() const988 bool QgsPostgresConn::hasTopology() const
989 {
990   // make sure info is up to date for the current connection
991   postgisVersion();
992   return mTopologyAvailable;
993 }
994 
995 /**
996  * Check to see if pointcloud is available
997  */
hasPointcloud() const998 bool QgsPostgresConn::hasPointcloud() const
999 {
1000   // make sure info is up to date for the current connection
1001   postgisVersion();
1002   return mPointcloudAvailable;
1003 }
1004 
1005 /**
1006  * Check to see if raster is available
1007  */
hasRaster() const1008 bool QgsPostgresConn::hasRaster() const
1009 {
1010   // make sure info is up to date for the current connection
1011   postgisVersion();
1012   return mRasterAvailable;
1013 }
1014 /* Functions for determining available features in postGIS */
postgisVersion() const1015 QString QgsPostgresConn::postgisVersion() const
1016 {
1017   QMutexLocker locker( &mLock );
1018   if ( mGotPostgisVersion )
1019     return mPostgisVersionInfo;
1020 
1021   mPostgresqlVersion = PQserverVersion( mConn );
1022 
1023   QgsPostgresResult result( PQexec( QStringLiteral( "SELECT postgis_version()" ), false ) );
1024   if ( result.PQntuples() != 1 )
1025   {
1026     QgsMessageLog::logMessage( tr( "No PostGIS support in the database." ), tr( "PostGIS" ) );
1027     mGotPostgisVersion = true;
1028     return QString();
1029   }
1030 
1031   mPostgisVersionInfo = result.PQgetvalue( 0, 0 );
1032 
1033   QgsDebugMsgLevel( "PostGIS version info: " + mPostgisVersionInfo, 2 );
1034 
1035 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
1036   QStringList postgisParts = mPostgisVersionInfo.split( ' ', QString::SkipEmptyParts );
1037 
1038   // Get major and minor version
1039   QStringList postgisVersionParts = postgisParts[0].split( '.', QString::SkipEmptyParts );
1040 #else
1041   QStringList postgisParts = mPostgisVersionInfo.split( ' ', Qt::SkipEmptyParts );
1042 
1043   // Get major and minor version
1044   QStringList postgisVersionParts = postgisParts[0].split( '.', Qt::SkipEmptyParts );
1045 #endif
1046   if ( postgisVersionParts.size() < 2 )
1047   {
1048     QgsMessageLog::logMessage( tr( "Could not parse postgis version string '%1'" ).arg( mPostgisVersionInfo ), tr( "PostGIS" ) );
1049     return QString();
1050   }
1051 
1052   mPostgisVersionMajor = postgisVersionParts[0].toInt();
1053   mPostgisVersionMinor = postgisVersionParts[1].toInt();
1054 
1055   mUseWkbHex = mPostgisVersionMajor < 1;
1056 
1057   // apparently PostGIS 1.5.2 doesn't report capabilities in postgis_version() anymore
1058   if ( mPostgisVersionMajor > 1 || ( mPostgisVersionMajor == 1 && mPostgisVersionMinor >= 5 ) )
1059   {
1060     result = PQexec( QStringLiteral( "SELECT postgis_geos_version(), postgis_proj_version()" ) );
1061     mGeosAvailable = result.PQntuples() == 1 && !result.PQgetisnull( 0, 0 );
1062     mProjAvailable = result.PQntuples() == 1 && !result.PQgetisnull( 0, 1 );
1063     QgsDebugMsgLevel( QStringLiteral( "geos:%1 proj:%2" )
1064                       .arg( mGeosAvailable ? result.PQgetvalue( 0, 0 ) : "none" )
1065                       .arg( mProjAvailable ? result.PQgetvalue( 0, 1 ) : "none" ), 2 );
1066   }
1067   else
1068   {
1069     // assume no capabilities
1070     mGeosAvailable = false;
1071 
1072     // parse out the capabilities and store them
1073     QStringList geos = postgisParts.filter( QStringLiteral( "GEOS" ) );
1074     if ( geos.size() == 1 )
1075     {
1076       mGeosAvailable = ( geos[0].indexOf( QLatin1String( "=1" ) ) > -1 );
1077     }
1078   }
1079 
1080   // checking for topology support
1081   QgsDebugMsgLevel( QStringLiteral( "Checking for topology support" ), 2 );
1082   mTopologyAvailable = false;
1083   if ( mPostgisVersionMajor > 1 )
1084   {
1085     QgsPostgresResult result(
1086       PQexec(
1087         QStringLiteral(
1088           "SELECT has_schema_privilege(n.oid, 'usage')"
1089           " AND has_table_privilege(t.oid, 'select')"
1090           " AND has_table_privilege(l.oid, 'select')"
1091           " FROM pg_namespace n, pg_class t, pg_class l"
1092           " WHERE n.nspname = 'topology'"
1093           " AND t.relnamespace = n.oid"
1094           " AND l.relnamespace = n.oid"
1095           " AND t.relname = 'topology'"
1096           " AND l.relname = 'layer'"
1097         ) ) );
1098     if ( result.PQntuples() >= 1 && result.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) )
1099     {
1100       mTopologyAvailable = true;
1101     }
1102   }
1103 
1104   if ( mTopologyAvailable )
1105   {
1106     QgsDebugMsgLevel( QStringLiteral( "Topology support available :)" ), 2 );
1107   }
1108   else
1109   {
1110     QgsDebugMsgLevel( QStringLiteral( "Topology support not available :(" ), 2 );
1111   }
1112 
1113   mGotPostgisVersion = true;
1114 
1115   if ( mPostgresqlVersion >= 90000 )
1116   {
1117     QgsDebugMsgLevel( QStringLiteral( "Checking for pointcloud support" ), 2 );
1118     result = PQexec( QStringLiteral(
1119                        "SELECT has_table_privilege(c.oid, 'select')"
1120                        " AND has_table_privilege(f.oid, 'select')"
1121                        " FROM pg_class c, pg_class f, pg_namespace n, pg_extension e"
1122                        " WHERE c.relnamespace = n.oid"
1123                        " AND c.relname = 'pointcloud_columns'"
1124                        " AND f.relnamespace = n.oid"
1125                        " AND f.relname = 'pointcloud_formats'"
1126                        " AND n.oid = e.extnamespace"
1127                        " AND e.extname = 'pointcloud'"
1128                      ), false );
1129     if ( result.PQntuples() >= 1 && result.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) )
1130     {
1131       mPointcloudAvailable = true;
1132       QgsDebugMsgLevel( QStringLiteral( "Pointcloud support available!" ), 2 );
1133     }
1134   }
1135 
1136   QgsDebugMsgLevel( QStringLiteral( "Checking for raster support" ), 2 );
1137   if ( mPostgisVersionMajor >= 2 )
1138   {
1139     result = PQexec( QStringLiteral(
1140                        "SELECT has_table_privilege(c.oid, 'select')"
1141                        " FROM pg_class c, pg_namespace n, pg_type t"
1142                        " WHERE c.relnamespace = n.oid"
1143                        " AND n.oid = t.typnamespace"
1144                        " AND c.relname = 'raster_columns'"
1145                        " AND t.typname = 'raster'"
1146                      ), false );
1147     if ( result.PQntuples() >= 1 && result.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) )
1148     {
1149       mRasterAvailable = true;
1150       QgsDebugMsgLevel( QStringLiteral( "Raster support available!" ), 2 );
1151     }
1152   }
1153 
1154   return mPostgisVersionInfo;
1155 }
1156 
quotedIdentifier(const QString & ident)1157 QString QgsPostgresConn::quotedIdentifier( const QString &ident )
1158 {
1159   QString result = ident;
1160   result.replace( '"', QLatin1String( "\"\"" ) );
1161   return result.prepend( '\"' ).append( '\"' );
1162 }
1163 
quotedString(const QString & v)1164 static QString quotedString( const QString &v )
1165 {
1166   QString result = v;
1167   result.replace( '\'', QLatin1String( "''" ) );
1168   if ( result.contains( '\\' ) )
1169     return result.replace( '\\', QLatin1String( "\\\\" ) ).prepend( "E'" ).append( '\'' );
1170   else
1171     return result.prepend( '\'' ).append( '\'' );
1172 }
1173 
doubleQuotedMapValue(const QString & v)1174 static QString doubleQuotedMapValue( const QString &v )
1175 {
1176   QString result = v;
1177   return "\"" + result.replace( '\\', QLatin1String( "\\\\\\\\" ) ).replace( '\"', QLatin1String( "\\\\\"" ) ).replace( '\'', QLatin1String( "\\'" ) ) + "\"";
1178 }
1179 
quotedMap(const QVariantMap & map)1180 static QString quotedMap( const QVariantMap &map )
1181 {
1182   QString ret;
1183   for ( QVariantMap::const_iterator i = map.constBegin(); i != map.constEnd(); ++i )
1184   {
1185     if ( !ret.isEmpty() )
1186     {
1187       ret += QLatin1Char( ',' );
1188     }
1189     ret.append( doubleQuotedMapValue( i.key() ) + "=>" +
1190                 doubleQuotedMapValue( i.value().toString() ) );
1191   }
1192   return "E'" + ret + "'::hstore";
1193 }
1194 
quotedList(const QVariantList & list)1195 static QString quotedList( const QVariantList &list )
1196 {
1197   QString ret;
1198   for ( QVariantList::const_iterator i = list.constBegin(); i != list.constEnd(); ++i )
1199   {
1200     if ( !ret.isEmpty() )
1201     {
1202       ret += QLatin1Char( ',' );
1203     }
1204 
1205     QString inner = i->toString();
1206     if ( inner.startsWith( '{' ) || i->type() == QVariant::Int || i->type() == QVariant::LongLong )
1207     {
1208       ret.append( inner );
1209     }
1210     else
1211     {
1212       ret.append( doubleQuotedMapValue( i->toString() ) );
1213     }
1214   }
1215   return "E'{" + ret + "}'";
1216 }
1217 
quotedValue(const QVariant & value)1218 QString QgsPostgresConn::quotedValue( const QVariant &value )
1219 {
1220   if ( value.isNull() )
1221     return QStringLiteral( "NULL" );
1222 
1223   switch ( value.type() )
1224   {
1225     case QVariant::Int:
1226     case QVariant::LongLong:
1227       return value.toString();
1228 
1229     case QVariant::DateTime:
1230       return quotedString( value.toDateTime().toString( Qt::ISODateWithMs ) );
1231 
1232     case QVariant::Bool:
1233       return value.toBool() ? "TRUE" : "FALSE";
1234 
1235     case QVariant::Map:
1236       return quotedMap( value.toMap() );
1237 
1238     case QVariant::StringList:
1239     case QVariant::List:
1240       return quotedList( value.toList() );
1241 
1242     case QVariant::Double:
1243     case QVariant::String:
1244     default:
1245       return quotedString( value.toString() );
1246   }
1247 }
1248 
quotedJsonValue(const QVariant & value)1249 QString QgsPostgresConn::quotedJsonValue( const QVariant &value )
1250 {
1251   if ( value.isNull() || !value.isValid() )
1252     return QStringLiteral( "null" );
1253   // where json is a string literal just construct it from that rather than dump
1254   if ( value.type() == QVariant::String )
1255   {
1256     QString valueStr = value.toString();
1257     if ( valueStr.at( 0 ) == '\"' && valueStr.at( valueStr.size() - 1 ) == '\"' )
1258     {
1259       return quotedString( value.toString() );
1260     }
1261   }
1262   const auto j = QgsJsonUtils::jsonFromVariant( value );
1263   return quotedString( QString::fromStdString( j.dump() ) );
1264 }
1265 
PQexec(const QString & query,bool logError,bool retry) const1266 PGresult *QgsPostgresConn::PQexec( const QString &query, bool logError, bool retry ) const
1267 {
1268   QMutexLocker locker( &mLock );
1269 
1270   QgsDebugMsgLevel( QStringLiteral( "Executing SQL: %1" ).arg( query ), 3 );
1271 
1272   PGresult *res = ::PQexec( mConn, query.toUtf8() );
1273 
1274   // libpq may return a non null ptr with conn status not OK so we need to check for it to allow a retry below
1275   if ( res && PQstatus() == CONNECTION_OK )
1276   {
1277     int errorStatus = PQresultStatus( res );
1278     if ( errorStatus != PGRES_COMMAND_OK && errorStatus != PGRES_TUPLES_OK )
1279     {
1280       if ( logError )
1281       {
1282         QgsMessageLog::logMessage( tr( "Erroneous query: %1 returned %2 [%3]" )
1283                                    .arg( query ).arg( errorStatus ).arg( PQresultErrorMessage( res ) ),
1284                                    tr( "PostGIS" ) );
1285       }
1286       else
1287       {
1288         QgsDebugMsg( QStringLiteral( "Not logged erroneous query: %1 returned %2 [%3]" )
1289                      .arg( query ).arg( errorStatus ).arg( PQresultErrorMessage( res ) ) );
1290       }
1291     }
1292     return res;
1293   }
1294   if ( PQstatus() != CONNECTION_OK )
1295   {
1296     if ( logError )
1297     {
1298       QgsMessageLog::logMessage( tr( "Connection error: %1 returned %2 [%3]" )
1299                                  .arg( query ).arg( PQstatus() ).arg( PQerrorMessage() ),
1300                                  tr( "PostGIS" ) );
1301     }
1302     else
1303     {
1304       QgsDebugMsg( QStringLiteral( "Connection error: %1 returned %2 [%3]" )
1305                    .arg( query ).arg( PQstatus() ).arg( PQerrorMessage() ) );
1306     }
1307   }
1308   else
1309   {
1310     if ( logError )
1311     {
1312       QgsMessageLog::logMessage( tr( "Query failed: %1\nError: no result buffer" ).arg( query ), tr( "PostGIS" ) );
1313     }
1314     else
1315     {
1316       QgsDebugMsg( QStringLiteral( "Not logged query failed: %1\nError: no result buffer" ).arg( query ) );
1317     }
1318   }
1319 
1320   if ( retry )
1321   {
1322     QgsMessageLog::logMessage( tr( "resetting bad connection." ), tr( "PostGIS" ) );
1323     ::PQreset( mConn );
1324     res = PQexec( query, logError, false );
1325     if ( PQstatus() == CONNECTION_OK )
1326     {
1327       if ( res )
1328       {
1329         QgsMessageLog::logMessage( tr( "retry after reset succeeded." ), tr( "PostGIS" ) );
1330         return res;
1331       }
1332       else
1333       {
1334         QgsMessageLog::logMessage( tr( "retry after reset failed again." ), tr( "PostGIS" ) );
1335         return nullptr;
1336       }
1337     }
1338     else
1339     {
1340       QgsMessageLog::logMessage( tr( "connection still bad after reset." ), tr( "PostGIS" ) );
1341     }
1342   }
1343   else
1344   {
1345     QgsMessageLog::logMessage( tr( "bad connection, not retrying." ), tr( "PostGIS" ) );
1346   }
1347   return nullptr;
1348 
1349 }
1350 
PQCancel()1351 int QgsPostgresConn::PQCancel()
1352 {
1353   // No locker: this is supposed to be thread safe
1354   int result = 0;
1355   auto cancel = ::PQgetCancel( mConn ) ;
1356   if ( cancel )
1357   {
1358     char errbuf[255];
1359     result = ::PQcancel( cancel, errbuf, 255 );
1360     if ( ! result )
1361       QgsDebugMsgLevel( QStringLiteral( "Error canceling the query:" ).arg( errbuf ), 3 );
1362   }
1363   ::PQfreeCancel( cancel );
1364   return result;
1365 }
1366 
openCursor(const QString & cursorName,const QString & sql)1367 bool QgsPostgresConn::openCursor( const QString &cursorName, const QString &sql )
1368 {
1369   QMutexLocker locker( &mLock ); // to protect access to mOpenCursors
1370   QString preStr;
1371 
1372   if ( mOpenCursors++ == 0 && !mTransaction )
1373   {
1374     QgsDebugMsgLevel( QStringLiteral( "Starting read-only transaction: %1" ).arg( mPostgresqlVersion ), 4 );
1375     if ( mPostgresqlVersion >= 80000 )
1376       preStr = QStringLiteral( "BEGIN READ ONLY;" );
1377     else
1378       preStr = QStringLiteral( "BEGIN;" );
1379   }
1380   QgsDebugMsgLevel( QStringLiteral( "Binary cursor %1 for %2" ).arg( cursorName, sql ), 3 );
1381   return PQexecNR( QStringLiteral( "%1DECLARE %2 BINARY CURSOR%3 FOR %4" ).
1382                    arg( preStr, cursorName, !mTransaction ? QString() : QStringLiteral( " WITH HOLD" ), sql ) );
1383 }
1384 
closeCursor(const QString & cursorName)1385 bool QgsPostgresConn::closeCursor( const QString &cursorName )
1386 {
1387   QMutexLocker locker( &mLock ); // to protect access to mOpenCursors
1388   QString postStr;
1389 
1390   if ( --mOpenCursors == 0 && !mTransaction )
1391   {
1392     QgsDebugMsgLevel( QStringLiteral( "Committing read-only transaction" ), 4 );
1393     postStr = QStringLiteral( ";COMMIT" );
1394   }
1395 
1396   if ( !PQexecNR( QStringLiteral( "CLOSE %1%2" ).arg( cursorName, postStr ) ) )
1397     return false;
1398 
1399   return true;
1400 }
1401 
uniqueCursorName()1402 QString QgsPostgresConn::uniqueCursorName()
1403 {
1404   QMutexLocker locker( &mLock ); // to protect access to mNextCursorId
1405   return QStringLiteral( "qgis_%1" ).arg( ++mNextCursorId );
1406 }
1407 
PQexecNR(const QString & query)1408 bool QgsPostgresConn::PQexecNR( const QString &query )
1409 {
1410   QMutexLocker locker( &mLock ); // to protect access to mOpenCursors
1411 
1412   QgsPostgresResult res( PQexec( query, false ) );
1413 
1414   ExecStatusType errorStatus = res.PQresultStatus();
1415   if ( errorStatus == PGRES_COMMAND_OK )
1416     return true;
1417 
1418   QgsMessageLog::logMessage( tr( "Query: %1 returned %2 [%3]" )
1419                              .arg( query )
1420                              .arg( errorStatus )
1421                              .arg( res.PQresultErrorMessage() ),
1422                              tr( "PostGIS" ) );
1423 
1424   if ( mOpenCursors )
1425   {
1426     QgsMessageLog::logMessage( tr( "%1 cursor states lost.\nSQL: %2\nResult: %3 (%4)" )
1427                                .arg( mOpenCursors ).arg( query ).arg( errorStatus )
1428                                .arg( res.PQresultErrorMessage() ), tr( "PostGIS" ) );
1429     mOpenCursors = 0;
1430   }
1431 
1432   if ( PQstatus() == CONNECTION_OK )
1433   {
1434     PQexecNR( QStringLiteral( "ROLLBACK" ) );
1435   }
1436 
1437   return false;
1438 }
1439 
PQgetResult()1440 PGresult *QgsPostgresConn::PQgetResult()
1441 {
1442   return ::PQgetResult( mConn );
1443 }
1444 
PQprepare(const QString & stmtName,const QString & query,int nParams,const Oid * paramTypes)1445 PGresult *QgsPostgresConn::PQprepare( const QString &stmtName, const QString &query, int nParams, const Oid *paramTypes )
1446 {
1447   QMutexLocker locker( &mLock );
1448 
1449   return ::PQprepare( mConn, stmtName.toUtf8(), query.toUtf8(), nParams, paramTypes );
1450 }
1451 
PQexecPrepared(const QString & stmtName,const QStringList & params)1452 PGresult *QgsPostgresConn::PQexecPrepared( const QString &stmtName, const QStringList &params )
1453 {
1454   QMutexLocker locker( &mLock );
1455 
1456   const char **param = new const char *[ params.size()];
1457   QList<QByteArray> qparam;
1458 
1459   qparam.reserve( params.size() );
1460   for ( int i = 0; i < params.size(); i++ )
1461   {
1462     qparam << params[i].toUtf8();
1463 
1464     if ( params[i].isNull() )
1465       param[i] = nullptr;
1466     else
1467       param[i] = qparam[i];
1468   }
1469 
1470   PGresult *res = ::PQexecPrepared( mConn, stmtName.toUtf8(), params.size(), param, nullptr, nullptr, 0 );
1471 
1472   delete [] param;
1473 
1474   return res;
1475 }
1476 
PQfinish()1477 void QgsPostgresConn::PQfinish()
1478 {
1479   QMutexLocker locker( &mLock );
1480 
1481   Q_ASSERT( mConn );
1482   ::PQfinish( mConn );
1483   mConn = nullptr;
1484 }
1485 
PQstatus() const1486 int QgsPostgresConn::PQstatus() const
1487 {
1488   QMutexLocker locker( &mLock );
1489 
1490   Q_ASSERT( mConn );
1491   return ::PQstatus( mConn );
1492 }
1493 
PQerrorMessage() const1494 QString QgsPostgresConn::PQerrorMessage() const
1495 {
1496   QMutexLocker locker( &mLock );
1497 
1498   Q_ASSERT( mConn );
1499   return QString::fromUtf8( ::PQerrorMessage( mConn ) );
1500 }
1501 
PQsendQuery(const QString & query)1502 int QgsPostgresConn::PQsendQuery( const QString &query )
1503 {
1504   QMutexLocker locker( &mLock );
1505   Q_ASSERT( mConn );
1506   return ::PQsendQuery( mConn, query.toUtf8() );
1507 }
1508 
begin()1509 bool QgsPostgresConn::begin()
1510 {
1511   QMutexLocker locker( &mLock );
1512   if ( mTransaction )
1513   {
1514     return PQexecNR( QStringLiteral( "SAVEPOINT transaction_savepoint" ) );
1515   }
1516   else
1517   {
1518     return PQexecNR( QStringLiteral( "BEGIN" ) );
1519   }
1520 }
1521 
commit()1522 bool QgsPostgresConn::commit()
1523 {
1524   QMutexLocker locker( &mLock );
1525   if ( mTransaction )
1526   {
1527     return PQexecNR( QStringLiteral( "RELEASE SAVEPOINT transaction_savepoint" ) );
1528   }
1529   else
1530   {
1531     return PQexecNR( QStringLiteral( "COMMIT" ) );
1532   }
1533 }
1534 
rollback()1535 bool QgsPostgresConn::rollback()
1536 {
1537   QMutexLocker locker( &mLock );
1538   if ( mTransaction )
1539   {
1540     return PQexecNR( QStringLiteral( "ROLLBACK TO SAVEPOINT transaction_savepoint" ) )
1541            && PQexecNR( QStringLiteral( "RELEASE SAVEPOINT transaction_savepoint" ) );
1542   }
1543   else
1544   {
1545     return PQexecNR( QStringLiteral( "ROLLBACK" ) );
1546   }
1547 }
1548 
getBinaryInt(QgsPostgresResult & queryResult,int row,int col)1549 qint64 QgsPostgresConn::getBinaryInt( QgsPostgresResult &queryResult, int row, int col )
1550 {
1551   QMutexLocker locker( &mLock );
1552   quint64 oid;
1553   char *p = PQgetvalue( queryResult.result(), row, col );
1554   size_t s = PQgetlength( queryResult.result(), row, col );
1555 
1556 #ifdef QGISDEBUG
1557   if ( QgsLogger::debugLevel() >= 4 )
1558   {
1559     QString buf;
1560     for ( size_t i = 0; i < s; i++ )
1561     {
1562       buf += QStringLiteral( "%1 " ).arg( *( unsigned char * )( p + i ), 0, 16, QLatin1Char( ' ' ) );
1563     }
1564 
1565     QgsDebugMsgLevel( QStringLiteral( "int in hex:%1" ).arg( buf ), 2 );
1566   }
1567 #endif
1568 
1569   switch ( s )
1570   {
1571     case 2:
1572       oid = *( quint16 * )p;
1573       if ( mSwapEndian )
1574         oid = ntohs( oid );
1575       /* cast to signed 16bit
1576        * See https://github.com/qgis/QGIS/issues/22258 */
1577       oid = ( qint16 )oid;
1578       break;
1579 
1580     case 6:
1581     {
1582       quint64 block  = *( quint32 * ) p;
1583       quint64 offset = *( quint16 * )( p + sizeof( quint32 ) );
1584 
1585       if ( mSwapEndian )
1586       {
1587         block = ntohl( block );
1588         offset = ntohs( offset );
1589       }
1590 
1591       oid = ( block << 16 ) + offset;
1592     }
1593     break;
1594 
1595     case 8:
1596     {
1597       quint32 oid0 = *( quint32 * ) p;
1598       quint32 oid1 = *( quint32 * )( p + sizeof( quint32 ) );
1599 
1600       if ( mSwapEndian )
1601       {
1602         QgsDebugMsgLevel( QStringLiteral( "swap oid0:%1 oid1:%2" ).arg( oid0 ).arg( oid1 ), 4 );
1603         oid0 = ntohl( oid0 );
1604         oid1 = ntohl( oid1 );
1605       }
1606 
1607       QgsDebugMsgLevel( QStringLiteral( "oid0:%1 oid1:%2" ).arg( oid0 ).arg( oid1 ), 4 );
1608       oid   = oid0;
1609       QgsDebugMsgLevel( QStringLiteral( "oid:%1" ).arg( oid ), 4 );
1610       oid <<= 32;
1611       QgsDebugMsgLevel( QStringLiteral( "oid:%1" ).arg( oid ), 4 );
1612       oid  |= oid1;
1613       QgsDebugMsgLevel( QStringLiteral( "oid:%1" ).arg( oid ), 4 );
1614     }
1615     break;
1616 
1617     default:
1618       QgsDebugMsg( QStringLiteral( "unexpected size %1" ).arg( s ) );
1619       //intentional fall-through
1620       FALLTHROUGH
1621     case 4:
1622       oid = *( quint32 * )p;
1623       if ( mSwapEndian )
1624         oid = ntohl( oid );
1625       /* cast to signed 32bit
1626        * See https://github.com/qgis/QGIS/issues/22258 */
1627       oid = ( qint32 )oid;
1628       break;
1629   }
1630 
1631   return oid;
1632 }
1633 
fieldExpressionForWhereClause(const QgsField & fld,QVariant::Type valueType,QString expr)1634 QString QgsPostgresConn::fieldExpressionForWhereClause( const QgsField &fld, QVariant::Type valueType, QString expr )
1635 {
1636   QString out;
1637   const QString &type = fld.typeName();
1638 
1639   if ( type == QLatin1String( "timestamp" ) || type == QLatin1String( "time" ) || type == QLatin1String( "date" ) )
1640   {
1641     out = expr.arg( quotedIdentifier( fld.name() ) );
1642     // if field and value havev incompatible types, rollback to text cast
1643     if ( valueType !=  QVariant::LastType && valueType != QVariant::DateTime && valueType != QVariant::Date && valueType != QVariant::Time )
1644     {
1645       out = out + "::text";
1646     }
1647   }
1648 
1649   else if ( type == QLatin1String( "int8" ) || type == QLatin1String( "serial8" ) //
1650             || type == QLatin1String( "int2" ) || type == QLatin1String( "int4" ) || type == QLatin1String( "oid" ) || type == QLatin1String( "serial" ) //
1651             || type == QLatin1String( "real" ) || type == QLatin1String( "double precision" ) || type == QLatin1String( "float4" ) || type == QLatin1String( "float8" ) //
1652             || type == QLatin1String( "numeric" ) )
1653   {
1654     out = expr.arg( quotedIdentifier( fld.name() ) );
1655     // if field and value havev incompatible types, rollback to text cast
1656     if ( valueType !=  QVariant::LastType && valueType != QVariant::Int && valueType != QVariant::LongLong && valueType != QVariant::Double )
1657     {
1658       out = out + "::text";
1659     }
1660   }
1661 
1662   else
1663   {
1664     out = fieldExpression( fld, expr ); // same as fieldExpression by default
1665   }
1666 
1667   return out;
1668 }
1669 
fieldExpression(const QgsField & fld,QString expr)1670 QString QgsPostgresConn::fieldExpression( const QgsField &fld, QString expr )
1671 {
1672   const QString &type = fld.typeName();
1673   expr = expr.arg( quotedIdentifier( fld.name() ) );
1674   if ( type == QLatin1String( "money" ) )
1675   {
1676     return QStringLiteral( "cash_out(%1)::text" ).arg( expr );
1677   }
1678   else if ( type.startsWith( '_' ) )
1679   {
1680     //TODO: add native support for arrays
1681     return QStringLiteral( "array_out(%1)::text" ).arg( expr );
1682   }
1683   else if ( type == QLatin1String( "bool" ) )
1684   {
1685     return QStringLiteral( "boolout(%1)::text" ).arg( expr );
1686   }
1687   else if ( type == QLatin1String( "geometry" ) )
1688   {
1689     return QStringLiteral( "%1(%2)" )
1690            .arg( majorVersion() < 2 ? "asewkt" : "st_asewkt",
1691                  expr );
1692   }
1693   else if ( type == QLatin1String( "geography" ) )
1694   {
1695     return QStringLiteral( "st_astext(%1)" ).arg( expr );
1696   }
1697   else if ( type == QLatin1String( "int8" ) )
1698   {
1699     return expr;
1700   }
1701   //TODO: add support for hstore
1702   //TODO: add support for json/jsonb
1703   else
1704   {
1705     return expr + "::text";
1706   }
1707 }
1708 
nativeTypes()1709 QList<QgsVectorDataProvider::NativeType> QgsPostgresConn::nativeTypes()
1710 {
1711   QList<QgsVectorDataProvider::NativeType> types;
1712 
1713   types     // integer types
1714       << QgsVectorDataProvider::NativeType( tr( "Whole number (smallint - 16bit)" ), QStringLiteral( "int2" ), QVariant::Int, -1, -1, 0, 0 )
1715       << QgsVectorDataProvider::NativeType( tr( "Whole number (integer - 32bit)" ), QStringLiteral( "int4" ), QVariant::Int, -1, -1, 0, 0 )
1716       << QgsVectorDataProvider::NativeType( tr( "Whole number (integer - 64bit)" ), QStringLiteral( "int8" ), QVariant::LongLong, -1, -1, 0, 0 )
1717       << QgsVectorDataProvider::NativeType( tr( "Decimal number (numeric)" ), QStringLiteral( "numeric" ), QVariant::Double, 1, 20, 0, 20 )
1718       << QgsVectorDataProvider::NativeType( tr( "Decimal number (decimal)" ), QStringLiteral( "decimal" ), QVariant::Double, 1, 20, 0, 20 )
1719 
1720       // floating point
1721       << QgsVectorDataProvider::NativeType( tr( "Decimal number (real)" ), QStringLiteral( "real" ), QVariant::Double, -1, -1, -1, -1 )
1722       << QgsVectorDataProvider::NativeType( tr( "Decimal number (double)" ), QStringLiteral( "double precision" ), QVariant::Double, -1, -1, -1, -1 )
1723 
1724       // string types
1725       << QgsVectorDataProvider::NativeType( tr( "Text, fixed length (char)" ), QStringLiteral( "char" ), QVariant::String, 1, 255, -1, -1 )
1726       << QgsVectorDataProvider::NativeType( tr( "Text, limited variable length (varchar)" ), QStringLiteral( "varchar" ), QVariant::String, 1, 255, -1, -1 )
1727       << QgsVectorDataProvider::NativeType( tr( "Text, unlimited length (text)" ), QStringLiteral( "text" ), QVariant::String, -1, -1, -1, -1 )
1728       << QgsVectorDataProvider::NativeType( tr( "Text, case-insensitive unlimited length (citext)" ), QStringLiteral( "citext" ), QVariant::String, -1, -1, -1, -1 )
1729 
1730       // date type
1731       << QgsVectorDataProvider::NativeType( tr( "Date" ), QStringLiteral( "date" ), QVariant::Date, -1, -1, -1, -1 )
1732       << QgsVectorDataProvider::NativeType( tr( "Time" ), QStringLiteral( "time" ), QVariant::Time, -1, -1, -1, -1 )
1733       << QgsVectorDataProvider::NativeType( tr( "Date & Time" ), QStringLiteral( "timestamp without time zone" ), QVariant::DateTime, -1, -1, -1, -1 )
1734 
1735       // complex types
1736       << QgsVectorDataProvider::NativeType( tr( "Map (hstore)" ), QStringLiteral( "hstore" ), QVariant::Map, -1, -1, -1, -1, QVariant::String )
1737       << QgsVectorDataProvider::NativeType( tr( "Array of number (integer - 32bit)" ), QStringLiteral( "int4[]" ), QVariant::List, -1, -1, -1, -1, QVariant::Int )
1738       << QgsVectorDataProvider::NativeType( tr( "Array of number (integer - 64bit)" ), QStringLiteral( "int8[]" ), QVariant::List, -1, -1, -1, -1, QVariant::LongLong )
1739       << QgsVectorDataProvider::NativeType( tr( "Array of number (double)" ), QStringLiteral( "double precision[]" ), QVariant::List, -1, -1, -1, -1, QVariant::Double )
1740       << QgsVectorDataProvider::NativeType( tr( "Array of text" ), QStringLiteral( "text[]" ), QVariant::StringList, -1, -1, -1, -1, QVariant::String )
1741 
1742       // boolean
1743       << QgsVectorDataProvider::NativeType( tr( "Boolean" ), QStringLiteral( "bool" ), QVariant::Bool, -1, -1, -1, -1 )
1744 
1745       // binary (bytea)
1746       << QgsVectorDataProvider::NativeType( tr( "Binary object (bytea)" ), QStringLiteral( "bytea" ), QVariant::ByteArray, -1, -1, -1, -1 )
1747       ;
1748 
1749   if ( pgVersion() >= 90200 )
1750   {
1751     types << QgsVectorDataProvider::NativeType( tr( "JSON (json)" ), QStringLiteral( "json" ), QVariant::Map, -1, -1, -1, -1, QVariant::String );
1752 
1753     if ( pgVersion() >= 90400 )
1754     {
1755       types << QgsVectorDataProvider::NativeType( tr( "JSON (jsonb)" ), QStringLiteral( "jsonb" ), QVariant::Map, -1, -1, -1, -1, QVariant::String );
1756     }
1757   }
1758   return types;
1759 }
1760 
deduceEndian()1761 void QgsPostgresConn::deduceEndian()
1762 {
1763   QMutexLocker locker( &mLock );
1764   // need to store the PostgreSQL endian format used in binary cursors
1765   // since it appears that starting with
1766   // version 7.4, binary cursors return data in XDR whereas previous versions
1767   // return data in the endian of the server
1768 
1769   QgsPostgresResult resOID;
1770   int queryCounter = 0;
1771   int errorCounter = 0;
1772   int oidStatus = 0;
1773   int oidSelectSet = 1 << 0;
1774   int oidBinaryCursorSet = 1 << 1;
1775   qint64 oidSelect = 0;
1776   qint64 oidBinaryCursor = 0;
1777 
1778   if ( 0 == PQsendQuery( QStringLiteral(
1779                            "SELECT regclass('pg_class')::oid AS oidselect;"
1780                            "BEGIN;"
1781                            "DECLARE oidcursor BINARY CURSOR FOR SELECT regclass('pg_class')::oid AS oidbinarycursor;"
1782                            "FETCH FORWARD 1 FROM oidcursor;"
1783                            "CLOSE oidcursor;"
1784                            "COMMIT;" ) ) )
1785     QgsDebugMsgLevel( QStringLiteral( "PQsendQuery(...) error %1" ).arg( PQerrorMessage() ), 2 );
1786 
1787   for ( ;; )
1788   {
1789     // PQgetResult() must be called repeatedly until it returns a null pointer
1790     resOID = PQgetResult();
1791 
1792     if ( resOID.result() == nullptr )
1793       break;
1794 
1795     queryCounter++;
1796     if ( resOID.PQresultStatus() == PGRES_FATAL_ERROR )
1797     {
1798       errorCounter++;
1799       QgsDebugMsgLevel( QStringLiteral( "QUERY #%1 PGRES_FATAL_ERROR %2" )
1800                         .arg( queryCounter )
1801                         .arg( PQerrorMessage().trimmed() ), 2 );
1802       continue;
1803     }
1804 
1805     if ( resOID.PQresultStatus() == PGRES_TUPLES_OK && resOID.PQnfields() && resOID.PQntuples() )
1806     {
1807       if ( resOID.PQfname( 0 ) == QLatin1String( "oidselect" ) )
1808       {
1809         oidSelect = resOID.PQgetvalue( 0, 0 ).toLongLong();
1810         oidStatus |= oidSelectSet;
1811       }
1812       if ( resOID.PQfname( 0 ) == QLatin1String( "oidbinarycursor" ) )
1813       {
1814         oidBinaryCursor = getBinaryInt( resOID, 0, 0 );
1815         oidStatus |= oidBinaryCursorSet;
1816       }
1817     }
1818   }
1819 
1820   if ( errorCounter == 0 && oidStatus == ( oidSelectSet | oidBinaryCursorSet ) )
1821   {
1822     mSwapEndian = mSwapEndian == ( oidSelect == oidBinaryCursor );
1823     return;
1824   }
1825 
1826   QgsDebugMsgLevel( QStringLiteral( "Back to old deduceEndian(): PQstatus() - %1, queryCounter = %2, errorCounter = %3" )
1827                     .arg( PQstatus() )
1828                     .arg( queryCounter )
1829                     .arg( errorCounter ), 2 );
1830 
1831   QgsPostgresResult res( PQexec( QStringLiteral( "select regclass('pg_class')::oid" ) ) );
1832   QString oidValue = res.PQgetvalue( 0, 0 );
1833 
1834   QgsDebugMsgLevel( QStringLiteral( "Creating binary cursor" ), 2 );
1835 
1836   // get the same value using a binary cursor
1837   openCursor( QStringLiteral( "oidcursor" ), QStringLiteral( "select regclass('pg_class')::oid" ) );
1838 
1839   QgsDebugMsgLevel( QStringLiteral( "Fetching a record and attempting to get check endian-ness" ), 2 );
1840 
1841   res = PQexec( QStringLiteral( "fetch forward 1 from oidcursor" ) );
1842 
1843   mSwapEndian = true;
1844   if ( res.PQntuples() > 0 )
1845   {
1846     // get the oid value from the binary cursor
1847     qint64 oid = getBinaryInt( res, 0, 0 );
1848 
1849     QgsDebugMsgLevel( QStringLiteral( "Got oid of %1 from the binary cursor" ).arg( oid ), 2 );
1850     QgsDebugMsgLevel( QStringLiteral( "First oid is %1" ).arg( oidValue ), 2 );
1851 
1852     // compare the two oid values to determine if we need to do an endian swap
1853     if ( oid != oidValue.toLongLong() )
1854       mSwapEndian = false;
1855   }
1856 
1857   closeCursor( QStringLiteral( "oidcursor" ) );
1858 }
1859 
retrieveLayerTypes(QgsPostgresLayerProperty & layerProperty,bool useEstimatedMetadata)1860 void QgsPostgresConn::retrieveLayerTypes( QgsPostgresLayerProperty &layerProperty, bool useEstimatedMetadata )
1861 {
1862   QVector<QgsPostgresLayerProperty *> vect;
1863   vect << &layerProperty;
1864   retrieveLayerTypes( vect, useEstimatedMetadata );
1865 }
1866 
retrieveLayerTypes(QVector<QgsPostgresLayerProperty * > & layerProperties,bool useEstimatedMetadata)1867 void QgsPostgresConn::retrieveLayerTypes( QVector<QgsPostgresLayerProperty *> &layerProperties, bool useEstimatedMetadata )
1868 {
1869   QString table;
1870   QString query;
1871 
1872   // Limit table row scan if useEstimatedMetadata
1873   const QString tableScanLimit { useEstimatedMetadata ? QStringLiteral( " LIMIT %1" ).arg( GEOM_TYPE_SELECT_LIMIT ) : QString() };
1874 
1875   int i = 0;
1876   for ( auto *layerPropertyPtr : layerProperties )
1877   {
1878     QgsPostgresLayerProperty &layerProperty = *layerPropertyPtr;
1879 
1880     if ( i++ )
1881       query += " UNION ";
1882 
1883     if ( !layerProperty.schemaName.isEmpty() )
1884     {
1885       table = QStringLiteral( "%1.%2" )
1886               .arg( quotedIdentifier( layerProperty.schemaName ),
1887                     quotedIdentifier( layerProperty.tableName ) );
1888     }
1889     else
1890     {
1891       // Query
1892       table = layerProperty.tableName;
1893     }
1894 
1895     if ( layerProperty.geometryColName.isEmpty() )
1896       continue;
1897 
1898     if ( layerProperty.isRaster )
1899     {
1900       QString sql;
1901 
1902       int srid = layerProperty.srids.value( 0, std::numeric_limits<int>::min() );
1903       // SRID is already known
1904       if ( srid != std::numeric_limits<int>::min() )
1905       {
1906         sql += QStringLiteral( "SELECT %1, array_agg( '%2:RASTER:-1'::text )" )
1907                .arg( i - 1 )
1908                .arg( srid );
1909       }
1910       else
1911       {
1912         if ( useEstimatedMetadata )
1913         {
1914           sql = QStringLiteral( "SELECT %1, "
1915                                 "array_agg(srid || ':RASTER:-1') "
1916                                 "FROM raster_columns "
1917                                 "WHERE r_raster_column = %2 AND r_table_schema = %3 AND r_table_name = %4" )
1918                 .arg( i - 1 )
1919                 .arg( quotedValue( layerProperty.geometryColName ) )
1920                 .arg( quotedValue( layerProperty.schemaName ) )
1921                 .arg( quotedValue( layerProperty.tableName ) );
1922         }
1923         else
1924         {
1925           sql = QStringLiteral( "SELECT %1, "
1926                                 "array_agg(DISTINCT st_srid(%2) || ':RASTER:-1') "
1927                                 "FROM %3 "
1928                                 "%2 IS NOT NULL "
1929                                 "%4"   // SQL clause
1930                                 "%5" )
1931                 .arg( i - 1 )
1932                 .arg( quotedIdentifier( layerProperty.geometryColName ) )
1933                 .arg( table )
1934                 .arg( layerProperty.sql.isEmpty() ? QString() : QStringLiteral( " AND %1" ).arg( layerProperty.sql ) )
1935                 .arg( tableScanLimit );
1936         }
1937       }
1938 
1939       QgsDebugMsgLevel( "Raster srids query: " + sql, 2 );
1940       query += sql;
1941     }
1942     else  // vectors
1943     {
1944       // our estimation ignores that a where clause might restrict the feature type or srid
1945       if ( useEstimatedMetadata )
1946       {
1947         table = QStringLiteral( "(SELECT %1 FROM %2 WHERE %3%1 IS NOT NULL%4) AS t" )
1948                 .arg( quotedIdentifier( layerProperty.geometryColName ),
1949                       table,
1950                       layerProperty.sql.isEmpty() ? QString() : QStringLiteral( " (%1) AND " ).arg( layerProperty.sql ) )
1951                 .arg( tableScanLimit );
1952       }
1953       else if ( !layerProperty.sql.isEmpty() )
1954       {
1955         table += QStringLiteral( " WHERE %1" ).arg( layerProperty.sql );
1956       }
1957 
1958       QString sql = QStringLiteral( "SELECT %1, " ).arg( i - 1 );
1959 
1960       bool castToGeometry = layerProperty.geometryColType == SctGeography ||
1961                             layerProperty.geometryColType == SctPcPatch;
1962 
1963       sql += QLatin1String( "array_agg(DISTINCT " );
1964 
1965       int srid = layerProperty.srids.value( 0, std::numeric_limits<int>::min() );
1966       if ( srid == std::numeric_limits<int>::min() )
1967       {
1968         sql += QStringLiteral( "%1(%2%3)::text" )
1969                .arg( majorVersion() < 2 ? "srid" : "st_srid",
1970                      quotedIdentifier( layerProperty.geometryColName ),
1971                      castToGeometry ?  "::geometry" : "" );
1972       }
1973       else
1974       {
1975         sql += QStringLiteral( "%1::text" )
1976                .arg( QString::number( srid ) );
1977       }
1978 
1979       sql += " || ':' || ";
1980 
1981       QgsWkbTypes::Type type = layerProperty.types.value( 0, QgsWkbTypes::Unknown );
1982       if ( type == QgsWkbTypes::Unknown )
1983       {
1984         // Note that we would like to apply a "LIMIT GEOM_TYPE_SELECT_LIMIT"
1985         // here, so that the previous "array_agg(DISTINCT" does not scan the
1986         // full table. However SQL does not allow that.
1987         // So we have to do a subselect on the table to add the LIMIT,
1988         // see comment in the following code.
1989         sql += QStringLiteral( "UPPER(geometrytype(%1%2))  || ':' || ST_Zmflag(%1%2)" )
1990                .arg( quotedIdentifier( layerProperty.geometryColName ),
1991                      castToGeometry ?  "::geometry" : "" );
1992       }
1993       else
1994       {
1995         sql += QStringLiteral( "%1::text  || ':-1'" )
1996                .arg( quotedValue( QgsPostgresConn::postgisWkbTypeName( type ) ) );
1997       }
1998 
1999 
2000       sql += QLatin1String( ") " );
2001 
2002       if ( type == QgsWkbTypes::Unknown )
2003       {
2004         // Subselect to limit the "array_agg(DISTINCT", see previous comment.
2005         sql += QStringLiteral( " FROM (SELECT %1 FROM %2%3) AS _unused" )
2006                .arg( quotedIdentifier( layerProperty.geometryColName ) )
2007                .arg( table )
2008                .arg( tableScanLimit );
2009       }
2010       else
2011       {
2012         sql += " FROM " + table;
2013       }
2014 
2015       QgsDebugMsgLevel( "Geometry types,srids and dims query: " + sql, 2 );
2016 
2017       query += sql;
2018     }
2019   }
2020 
2021   QgsDebugMsgLevel( "Layer types,srids and dims query: " + query, 3 );
2022 
2023   QgsPostgresResult res( PQexec( query ) );
2024   if ( res.PQresultStatus() != PGRES_TUPLES_OK )
2025   {
2026     // TODO: print some error here ?
2027     return;
2028   }
2029 
2030   for ( int i = 0; i < res.PQntuples(); i++ )
2031   {
2032     int idx = res.PQgetvalue( i, 0 ).toInt();
2033     auto srids_and_types = QgsPostgresStringUtils::parseArray( res.PQgetvalue( i, 1 ) );
2034     QgsPostgresLayerProperty &layerProperty = *layerProperties[idx];
2035 
2036     QgsDebugMsgLevel( QStringLiteral(
2037                         "Layer %1.%2.%3 has %4 srid/type combinations"
2038                       )
2039                       .arg( layerProperty.schemaName,
2040                             layerProperty.tableName,
2041                             layerProperty.geometryColName )
2042                       .arg( srids_and_types.length() )
2043                       , 3
2044                     );
2045 
2046     /* Gather found types */
2047     QList< std::pair<QgsWkbTypes::Type, int> > foundCombinations;
2048     for ( const auto &sridAndTypeVariant : srids_and_types )
2049     {
2050       QString sridAndTypeString = sridAndTypeVariant.toString();
2051 
2052       QgsDebugMsgLevel( QStringLiteral(
2053                           "Analyzing layer's %1.%2.%3 sridAndType %4"
2054                           " against %6 found combinations"
2055                         )
2056                         .arg( layerProperty.schemaName,
2057                               layerProperty.tableName,
2058                               layerProperty.geometryColName )
2059                         .arg( sridAndTypeString )
2060                         .arg( foundCombinations.length() )
2061                         , 3
2062                       );
2063 
2064       if ( sridAndTypeString == "NULL" )
2065         continue;
2066 
2067       const QStringList sridAndType = sridAndTypeString.split( ':' );
2068       Q_ASSERT( sridAndType.size() == 3 );
2069       const int srid = sridAndType[0].toInt();
2070       QString typeString = sridAndType[1];
2071       const int zmFlags = sridAndType[2].toInt();
2072 
2073       switch ( zmFlags )
2074       {
2075         case 1:
2076           typeString.append( 'M' );
2077           break;
2078         case 2:
2079           typeString.append( 'Z' );
2080           break;
2081         case 3:
2082           typeString.append( QStringLiteral( "ZM" ) );
2083           break;
2084         default:
2085         case 0:
2086         case -1:
2087           break;
2088       }
2089 
2090       auto type = QgsPostgresConn::wkbTypeFromPostgis( typeString );
2091       auto flatType = QgsWkbTypes::flatType( type );
2092       auto multiType = QgsWkbTypes::multiType( flatType );
2093       auto curveType = QgsWkbTypes::curveType( flatType );
2094       auto multiCurveType = QgsWkbTypes::multiType( curveType );
2095 
2096       // if both multi and single types exists, go for the multi type,
2097       // so that st_multi can be applied if necessary.
2098 
2099       // if both flat and curve types exists, go for the curve type,
2100       // so that st_multi can be applied if necessary.
2101 
2102       int j;
2103       for ( j = 0; j < foundCombinations.length(); j++ )
2104       {
2105         auto foundPair = foundCombinations.at( j );
2106         if ( foundPair.second != srid )
2107           continue; // srid must match
2108 
2109         auto knownType = foundPair.first;
2110         if ( type == knownType )
2111           break; // found
2112 
2113         auto knownMultiType = QgsWkbTypes::multiType( knownType );
2114         auto knownCurveType = QgsWkbTypes::curveType( knownType );
2115         auto knownMultiCurveType = QgsWkbTypes::multiType( knownCurveType );
2116 
2117         if ( multiCurveType == knownMultiCurveType )
2118         {
2119           QgsDebugMsgLevel( QStringLiteral(
2120                               "Upgrading type[%1] of layer %2.%3.%4 "
2121                               "to multiCurved type %5" )
2122                             .arg( j )
2123                             .arg( layerProperty.schemaName,
2124                                   layerProperty.tableName,
2125                                   layerProperty.geometryColName )
2126                             .arg( multiCurveType ), 3
2127                           );
2128           foundCombinations[j].first = multiCurveType;
2129           break;
2130         }
2131         else if ( multiType == knownMultiType )
2132         {
2133           QgsDebugMsgLevel( QStringLiteral(
2134                               "Upgrading type[%1] of layer %2.%3.%4 "
2135                               "to multi type %5" )
2136                             .arg( j )
2137                             .arg( layerProperty.schemaName,
2138                                   layerProperty.tableName,
2139                                   layerProperty.geometryColName )
2140                             .arg( multiType ), 3
2141                           );
2142           foundCombinations[j].first = multiType;
2143           break;
2144         }
2145         else if ( curveType == knownCurveType )
2146         {
2147           QgsDebugMsgLevel( QStringLiteral(
2148                               "Upgrading type[%1] of layer %2.%3.%4 "
2149                               "to curved type %5" )
2150                             .arg( j )
2151                             .arg( layerProperty.schemaName,
2152                                   layerProperty.tableName,
2153                                   layerProperty.geometryColName )
2154                             .arg( multiType ), 3
2155                           );
2156           foundCombinations[j].first = curveType;
2157           break;
2158         }
2159       }
2160 
2161       if ( j < foundCombinations.length() )
2162       {
2163         QgsDebugMsgLevel( QStringLiteral(
2164                             "Pre-existing compatible combination %1/%2 "
2165                             "found for layer %3.%4.%5 "
2166                           )
2167                           .arg( j ) .arg( foundCombinations.length() )
2168                           .arg( layerProperty.schemaName,
2169                                 layerProperty.tableName,
2170                                 layerProperty.geometryColName ), 3
2171                         );
2172         continue; // already found
2173       }
2174 
2175       QgsDebugMsgLevel( QStringLiteral(
2176                           "Setting typeSridCombination[%1] of layer %2.%3.%4 "
2177                           "to srid %5 and type %6" )
2178                         .arg( j )
2179                         .arg( layerProperty.schemaName,
2180                               layerProperty.tableName,
2181                               layerProperty.geometryColName )
2182                         .arg( srid )
2183                         .arg( type ), 3
2184                       );
2185 
2186       foundCombinations << std::make_pair( type, srid );
2187     }
2188 
2189     QgsDebugMsgLevel( QStringLiteral(
2190                         "Completed scan of %1 srid/type combinations "
2191                         "for layer of layer %2.%3.%4 " )
2192                       .arg( srids_and_types.length() )
2193                       .arg( layerProperty.schemaName,
2194                             layerProperty.tableName,
2195                             layerProperty.geometryColName ), 2
2196                     );
2197 
2198     /* Rewrite srids and types to match found combinations
2199      * of srids and types */
2200     layerProperty.srids.clear();
2201     layerProperty.types.clear();
2202     for ( const auto &comb : foundCombinations )
2203     {
2204       layerProperty.types << comb.first;
2205       layerProperty.srids << comb.second;
2206     }
2207     QgsDebugMsgLevel( QStringLiteral(
2208                         "Final layer %1.%2.%3 types: %4" )
2209                       .arg( layerProperty.schemaName,
2210                             layerProperty.tableName,
2211                             layerProperty.geometryColName )
2212                       .arg( layerProperty.types.length() ), 2
2213                     );
2214     QgsDebugMsgLevel( QStringLiteral(
2215                         "Final layer %1.%2.%3 srids: %4" )
2216                       .arg( layerProperty.schemaName,
2217                             layerProperty.tableName,
2218                             layerProperty.geometryColName )
2219                       .arg( layerProperty.srids.length() ), 2
2220                     );
2221   }
2222 }
2223 
postgisWkbType(QgsWkbTypes::Type wkbType,QString & geometryType,int & dim)2224 void QgsPostgresConn::postgisWkbType( QgsWkbTypes::Type wkbType, QString &geometryType, int &dim )
2225 {
2226   dim = 2;
2227   QgsWkbTypes::Type flatType = QgsWkbTypes::flatType( wkbType );
2228   switch ( flatType )
2229   {
2230     case QgsWkbTypes::Point:
2231       geometryType = QStringLiteral( "POINT" );
2232       break;
2233 
2234     case QgsWkbTypes::LineString:
2235       geometryType = QStringLiteral( "LINESTRING" );
2236       break;
2237 
2238     case QgsWkbTypes::Polygon:
2239       geometryType = QStringLiteral( "POLYGON" );
2240       break;
2241 
2242     case QgsWkbTypes::MultiPoint:
2243       geometryType = QStringLiteral( "MULTIPOINT" );
2244       break;
2245 
2246     case QgsWkbTypes::MultiLineString:
2247       geometryType = QStringLiteral( "MULTILINESTRING" );
2248       break;
2249 
2250     case QgsWkbTypes::MultiPolygon:
2251       geometryType = QStringLiteral( "MULTIPOLYGON" );
2252       break;
2253 
2254     case QgsWkbTypes::CircularString:
2255       geometryType = QStringLiteral( "CIRCULARSTRING" );
2256       break;
2257 
2258     case QgsWkbTypes::CompoundCurve:
2259       geometryType = QStringLiteral( "COMPOUNDCURVE" );
2260       break;
2261 
2262     case QgsWkbTypes::CurvePolygon:
2263       geometryType = QStringLiteral( "CURVEPOLYGON" );
2264       break;
2265 
2266     case QgsWkbTypes::MultiCurve:
2267       geometryType = QStringLiteral( "MULTICURVE" );
2268       break;
2269 
2270     case QgsWkbTypes::MultiSurface:
2271       geometryType = QStringLiteral( "MULTISURFACE" );
2272       break;
2273 
2274     case QgsWkbTypes::Unknown:
2275       geometryType = QStringLiteral( "GEOMETRY" );
2276       break;
2277 
2278     case QgsWkbTypes::NoGeometry:
2279     default:
2280       dim = 0;
2281       break;
2282   }
2283 
2284   if ( QgsWkbTypes::hasZ( wkbType ) && QgsWkbTypes::hasM( wkbType ) )
2285   {
2286     geometryType += QLatin1String( "ZM" );
2287     dim = 4;
2288   }
2289   else if ( QgsWkbTypes::hasZ( wkbType ) )
2290   {
2291     geometryType += QLatin1Char( 'Z' );
2292     dim = 3;
2293   }
2294   else if ( QgsWkbTypes::hasM( wkbType ) )
2295   {
2296     geometryType += QLatin1Char( 'M' );
2297     dim = 3;
2298   }
2299   else if ( wkbType >= QgsWkbTypes::Point25D && wkbType <= QgsWkbTypes::MultiPolygon25D )
2300   {
2301     dim = 3;
2302   }
2303 }
2304 
postgisWkbTypeName(QgsWkbTypes::Type wkbType)2305 QString QgsPostgresConn::postgisWkbTypeName( QgsWkbTypes::Type wkbType )
2306 {
2307   QString geometryType;
2308   int dim;
2309 
2310   postgisWkbType( wkbType, geometryType, dim );
2311 
2312   return geometryType;
2313 }
2314 
postgisTypeFilter(QString geomCol,QgsWkbTypes::Type wkbType,bool castToGeometry)2315 QString QgsPostgresConn::postgisTypeFilter( QString geomCol, QgsWkbTypes::Type wkbType, bool castToGeometry )
2316 {
2317   geomCol = quotedIdentifier( geomCol );
2318   if ( castToGeometry )
2319     geomCol += QLatin1String( "::geometry" );
2320 
2321   QgsWkbTypes::GeometryType geomType = QgsWkbTypes::geometryType( wkbType );
2322   switch ( geomType )
2323   {
2324     case QgsWkbTypes::PointGeometry:
2325       return QStringLiteral( "upper(geometrytype(%1)) IN ('POINT','POINTZ','POINTM','POINTZM','MULTIPOINT','MULTIPOINTZ','MULTIPOINTM','MULTIPOINTZM')" ).arg( geomCol );
2326     case QgsWkbTypes::LineGeometry:
2327       return QStringLiteral( "upper(geometrytype(%1)) IN ('LINESTRING','LINESTRINGZ','LINESTRINGM','LINESTRINGZM','CIRCULARSTRING','CIRCULARSTRINGZ','CIRCULARSTRINGM','CIRCULARSTRINGZM','COMPOUNDCURVE','COMPOUNDCURVEZ','COMPOUNDCURVEM','COMPOUNDCURVEZM','MULTILINESTRING','MULTILINESTRINGZ','MULTILINESTRINGM','MULTILINESTRINGZM','MULTICURVE','MULTICURVEZ','MULTICURVEM','MULTICURVEZM')" ).arg( geomCol );
2328     case QgsWkbTypes::PolygonGeometry:
2329       return QStringLiteral( "upper(geometrytype(%1)) IN ('POLYGON','POLYGONZ','POLYGONM','POLYGONZM','CURVEPOLYGON','CURVEPOLYGONZ','CURVEPOLYGONM','CURVEPOLYGONZM','MULTIPOLYGON','MULTIPOLYGONZ','MULTIPOLYGONM','MULTIPOLYGONZM','MULTIPOLYGONM','MULTISURFACE','MULTISURFACEZ','MULTISURFACEM','MULTISURFACEZM','POLYHEDRALSURFACE','TIN')" ).arg( geomCol );
2330     case QgsWkbTypes::NullGeometry:
2331       return QStringLiteral( "geometrytype(%1) IS NULL" ).arg( geomCol );
2332     default: //unknown geometry
2333       return QString();
2334   }
2335 }
2336 
postgisWkbTypeDim(QgsWkbTypes::Type wkbType)2337 int QgsPostgresConn::postgisWkbTypeDim( QgsWkbTypes::Type wkbType )
2338 {
2339   QString geometryType;
2340   int dim;
2341 
2342   postgisWkbType( wkbType, geometryType, dim );
2343 
2344   return dim;
2345 }
2346 
wkbTypeFromPostgis(const QString & type)2347 QgsWkbTypes::Type QgsPostgresConn::wkbTypeFromPostgis( const QString &type )
2348 {
2349   // Polyhedral surfaces and TIN are stored in PostGIS as geometry collections
2350   // of Polygons and Triangles.
2351   // So, since QGIS does not natively support PS and TIN, but we would like to open them if possible,
2352   // we consider them as multipolygons. WKB will be converted by the feature iterator
2353   if ( ( type == QLatin1String( "POLYHEDRALSURFACE" ) ) || ( type == QLatin1String( "TIN" ) ) )
2354   {
2355     return QgsWkbTypes::MultiPolygon;
2356   }
2357   else if ( ( type == QLatin1String( "POLYHEDRALSURFACEZ" ) ) || ( type == QLatin1String( "TINZ" ) ) )
2358   {
2359     return QgsWkbTypes::MultiPolygonZ;
2360   }
2361   else if ( ( type == QLatin1String( "POLYHEDRALSURFACEM" ) ) || ( type == QLatin1String( "TINM" ) ) )
2362   {
2363     return QgsWkbTypes::MultiPolygonM;
2364   }
2365   else if ( ( type == QLatin1String( "POLYHEDRALSURFACEZM" ) ) || ( type == QLatin1String( "TINZM" ) ) )
2366   {
2367     return QgsWkbTypes::MultiPolygonZM;
2368   }
2369   else if ( type == QLatin1String( "TRIANGLE" ) )
2370   {
2371     return QgsWkbTypes::Polygon;
2372   }
2373   else if ( type == QLatin1String( "TRIANGLEZ" ) )
2374   {
2375     return QgsWkbTypes::PolygonZ;
2376   }
2377   else if ( type == QLatin1String( "TRIANGLEM" ) )
2378   {
2379     return QgsWkbTypes::PolygonM;
2380   }
2381   else if ( type == QLatin1String( "TRIANGLEZM" ) )
2382   {
2383     return QgsWkbTypes::PolygonZM;
2384   }
2385   return QgsWkbTypes::parseType( type );
2386 }
2387 
wkbTypeFromOgcWkbType(unsigned int wkbType)2388 QgsWkbTypes::Type QgsPostgresConn::wkbTypeFromOgcWkbType( unsigned int wkbType )
2389 {
2390   // PolyhedralSurface => MultiPolygon
2391   if ( wkbType % 1000 == 15 )
2392     return ( QgsWkbTypes::Type )( wkbType / 1000 * 1000 + QgsWkbTypes::MultiPolygon );
2393   // TIN => MultiPolygon
2394   if ( wkbType % 1000 == 16 )
2395     return ( QgsWkbTypes::Type )( wkbType / 1000 * 1000 + QgsWkbTypes::MultiPolygon );
2396   // Triangle => Polygon
2397   if ( wkbType % 1000 == 17 )
2398     return ( QgsWkbTypes::Type )( wkbType / 1000 * 1000 + QgsWkbTypes::Polygon );
2399   return ( QgsWkbTypes::Type ) wkbType;
2400 }
2401 
displayStringForWkbType(QgsWkbTypes::Type type)2402 QString QgsPostgresConn::displayStringForWkbType( QgsWkbTypes::Type type )
2403 {
2404   return QgsWkbTypes::displayString( QgsWkbTypes::Type( type ) );
2405 }
2406 
displayStringForGeomType(QgsPostgresGeometryColumnType type)2407 QString QgsPostgresConn::displayStringForGeomType( QgsPostgresGeometryColumnType type )
2408 {
2409   switch ( type )
2410   {
2411     case SctNone:
2412       return tr( "None" );
2413     case SctGeometry:
2414       return tr( "Geometry" );
2415     case SctGeography:
2416       return tr( "Geography" );
2417     case SctTopoGeometry:
2418       return tr( "TopoGeometry" );
2419     case SctPcPatch:
2420       return tr( "PcPatch" );
2421     case SctRaster:
2422       return tr( "Raster" );
2423   }
2424 
2425   Q_ASSERT( !"unexpected geometry column type" );
2426   return QString();
2427 }
2428 
wkbTypeFromGeomType(QgsWkbTypes::GeometryType geomType)2429 QgsWkbTypes::Type QgsPostgresConn::wkbTypeFromGeomType( QgsWkbTypes::GeometryType geomType )
2430 {
2431   switch ( geomType )
2432   {
2433     case QgsWkbTypes::PointGeometry:
2434       return QgsWkbTypes::Point;
2435     case QgsWkbTypes::LineGeometry:
2436       return QgsWkbTypes::LineString;
2437     case QgsWkbTypes::PolygonGeometry:
2438       return QgsWkbTypes::Polygon;
2439     case QgsWkbTypes::NullGeometry:
2440       return QgsWkbTypes::NoGeometry;
2441     case QgsWkbTypes::UnknownGeometry:
2442       return QgsWkbTypes::Unknown;
2443   }
2444 
2445   Q_ASSERT( !"unexpected geomType" );
2446   return QgsWkbTypes::Unknown;
2447 }
2448 
connectionList()2449 QStringList QgsPostgresConn::connectionList()
2450 {
2451   QgsSettings settings;
2452   settings.beginGroup( QStringLiteral( "PostgreSQL/connections" ) );
2453   return settings.childGroups();
2454 }
2455 
selectedConnection()2456 QString QgsPostgresConn::selectedConnection()
2457 {
2458   QgsSettings settings;
2459   return settings.value( QStringLiteral( "PostgreSQL/connections/selected" ) ).toString();
2460 }
2461 
setSelectedConnection(const QString & name)2462 void QgsPostgresConn::setSelectedConnection( const QString &name )
2463 {
2464   QgsSettings settings;
2465   return settings.setValue( QStringLiteral( "PostgreSQL/connections/selected" ), name );
2466 }
2467 
connUri(const QString & connName)2468 QgsDataSourceUri QgsPostgresConn::connUri( const QString &connName )
2469 {
2470   QgsDebugMsgLevel( "theConnName = " + connName, 2 );
2471 
2472   QgsSettings settings;
2473 
2474   QString key = "/PostgreSQL/connections/" + connName;
2475 
2476   QString service = settings.value( key + "/service" ).toString();
2477   QString host = settings.value( key + "/host" ).toString();
2478   QString port = settings.value( key + "/port" ).toString();
2479   if ( port.length() == 0 )
2480   {
2481     port = QStringLiteral( "5432" );
2482   }
2483   QString database = settings.value( key + "/database" ).toString();
2484 
2485   bool estimatedMetadata = useEstimatedMetadata( connName );
2486   QgsDataSourceUri::SslMode sslmode = settings.enumValue( key + "/sslmode", QgsDataSourceUri::SslPrefer );
2487 
2488   QString username;
2489   QString password;
2490   if ( settings.value( key + "/saveUsername" ).toString() == QLatin1String( "true" ) )
2491   {
2492     username = settings.value( key + "/username" ).toString();
2493   }
2494 
2495   if ( settings.value( key + "/savePassword" ).toString() == QLatin1String( "true" ) )
2496   {
2497     password = settings.value( key + "/password" ).toString();
2498   }
2499 
2500   // Old save setting
2501   if ( settings.contains( key + "/save" ) )
2502   {
2503     username = settings.value( key + "/username" ).toString();
2504 
2505     if ( settings.value( key + "/save" ).toString() == QLatin1String( "true" ) )
2506     {
2507       password = settings.value( key + "/password" ).toString();
2508     }
2509   }
2510 
2511   QString authcfg = settings.value( key + "/authcfg" ).toString();
2512 
2513   QgsDataSourceUri uri;
2514   if ( !service.isEmpty() )
2515   {
2516     uri.setConnection( service, database, username, password, sslmode, authcfg );
2517   }
2518   else
2519   {
2520     uri.setConnection( host, port, database, username, password, sslmode, authcfg );
2521   }
2522   uri.setUseEstimatedMetadata( estimatedMetadata );
2523 
2524   return uri;
2525 }
2526 
publicSchemaOnly(const QString & connName)2527 bool QgsPostgresConn::publicSchemaOnly( const QString &connName )
2528 {
2529   QgsSettings settings;
2530   return settings.value( "/PostgreSQL/connections/" + connName + "/publicOnly", false ).toBool();
2531 }
2532 
geometryColumnsOnly(const QString & connName)2533 bool QgsPostgresConn::geometryColumnsOnly( const QString &connName )
2534 {
2535   QgsSettings settings;
2536 
2537   return settings.value( "/PostgreSQL/connections/" + connName + "/geometryColumnsOnly", false ).toBool();
2538 }
2539 
dontResolveType(const QString & connName)2540 bool QgsPostgresConn::dontResolveType( const QString &connName )
2541 {
2542   QgsSettings settings;
2543 
2544   return settings.value( "/PostgreSQL/connections/" + connName + "/dontResolveType", false ).toBool();
2545 }
2546 
useEstimatedMetadata(const QString & connName)2547 bool QgsPostgresConn::useEstimatedMetadata( const QString &connName )
2548 {
2549   QgsSettings settings;
2550 
2551   return settings.value( "/PostgreSQL/connections/" + connName + "/estimatedMetadata", false ).toBool();
2552 }
2553 
2554 
allowGeometrylessTables(const QString & connName)2555 bool QgsPostgresConn::allowGeometrylessTables( const QString &connName )
2556 {
2557   QgsSettings settings;
2558   return settings.value( "/PostgreSQL/connections/" + connName + "/allowGeometrylessTables", false ).toBool();
2559 }
2560 
allowProjectsInDatabase(const QString & connName)2561 bool QgsPostgresConn::allowProjectsInDatabase( const QString &connName )
2562 {
2563   QgsSettings settings;
2564   return settings.value( "/PostgreSQL/connections/" + connName + "/projectsInDatabase", false ).toBool();
2565 }
2566 
deleteConnection(const QString & connName)2567 void QgsPostgresConn::deleteConnection( const QString &connName )
2568 {
2569   QgsSettings settings;
2570 
2571   QString key = "/PostgreSQL/connections/" + connName;
2572   settings.remove( key + "/service" );
2573   settings.remove( key + "/host" );
2574   settings.remove( key + "/port" );
2575   settings.remove( key + "/database" );
2576   settings.remove( key + "/username" );
2577   settings.remove( key + "/password" );
2578   settings.remove( key + "/sslmode" );
2579   settings.remove( key + "/publicOnly" );
2580   settings.remove( key + "/geometryColumnsOnly" );
2581   settings.remove( key + "/allowGeometrylessTables" );
2582   settings.remove( key + "/estimatedMetadata" );
2583   settings.remove( key + "/saveUsername" );
2584   settings.remove( key + "/savePassword" );
2585   settings.remove( key + "/save" );
2586   settings.remove( key + "/authcfg" );
2587   settings.remove( key + "/keys" );
2588   settings.remove( key );
2589 }
2590 
cancel()2591 bool QgsPostgresConn::cancel()
2592 {
2593   QMutexLocker locker( &mLock );
2594   PGcancel *c = ::PQgetCancel( mConn );
2595   if ( !c )
2596   {
2597     QgsMessageLog::logMessage( tr( "Query could not be canceled [%1]" ).arg( tr( "PQgetCancel failed" ) ),
2598                                tr( "PostGIS" ) );
2599     return false;
2600   }
2601 
2602   char errbuf[256];
2603   int res = ::PQcancel( c, errbuf, sizeof errbuf );
2604   ::PQfreeCancel( c );
2605 
2606   if ( !res )
2607     QgsMessageLog::logMessage( tr( "Query could not be canceled [%1]" ).arg( errbuf ), tr( "PostGIS" ) );
2608 
2609   return res == 0;
2610 }
2611 
currentDatabase() const2612 QString QgsPostgresConn::currentDatabase() const
2613 {
2614   QMutexLocker locker( &mLock );
2615   QString database;
2616   QString sql = "SELECT current_database()";
2617   QgsPostgresResult res( PQexec( sql ) );
2618 
2619   if ( res.PQresultStatus() == PGRES_TUPLES_OK )
2620   {
2621     database = res.PQgetvalue( 0, 0 );
2622   }
2623   else
2624   {
2625     QgsMessageLog::logMessage( tr( "SQL: %1\nresult: %2\nerror: %3\n" ).arg( sql ).arg( res.PQresultStatus() ).arg( res.PQresultErrorMessage() ), tr( "PostGIS" ) );
2626   }
2627 
2628   return database;
2629 }
2630