Statistics
| Branch: | Tag: | Revision:

qgis / src / providers / postgres / qgspostgresprovider.cpp @ master

History | View | Annotate | Download (211 KB)

1
/***************************************************************************
2
  qgspostgresprovider.cpp  -  QGIS data provider for PostgreSQL/PostGIS layers
3
                             -------------------
4
    begin                : 2004/01/07
5
    copyright            : (C) 2004 by Gary E.Sherman
6
    email                : sherman at mrcc.com
7
 ***************************************************************************/
8

    
9
/***************************************************************************
10
 *                                                                         *
11
 *   This program is free software; you can redistribute it and/or modify  *
12
 *   it under the terms of the GNU General Public License as published by  *
13
 *   the Free Software Foundation; either version 2 of the License, or     *
14
 *   (at your option) any later version.                                   *
15
 *                                                                         *
16
 ***************************************************************************/
17

    
18
#include "qgsapplication.h"
19
#include "qgsfeature.h"
20
#include "qgsfield.h"
21
#include "qgsgeometry.h"
22
#include "qgsmessagelog.h"
23
#include "qgsprojectstorageregistry.h"
24
#include "qgslayermetadataproviderregistry.h"
25
#include "qgsrectangle.h"
26
#include "qgscoordinatereferencesystem.h"
27
#include "qgsxmlutils.h"
28
#include "qgsvectorlayer.h"
29
#include "qgspostgresprovider.h"
30
#include "moc_qgspostgresprovider.cpp"
31
#include "qgspostgresconn.h"
32
#include "qgspostgresconnpool.h"
33
#include "qgspostgresdataitems.h"
34
#include "qgspostgresfeatureiterator.h"
35
#include "qgspostgrestransaction.h"
36
#include "qgspostgreslistener.h"
37
#include "qgspostgresprojectstorage.h"
38
#include "qgspostgresproviderconnection.h"
39
#include "qgslogger.h"
40
#include "qgsfeedback.h"
41
#include "qgsstringutils.h"
42
#include "qgsjsonutils.h"
43
#include "qgsdbquerylog.h"
44
#include "qgsdbquerylog_p.h"
45
#include "qgspostgreslayermetadataprovider.h"
46

    
47
#include "qgspostgresprovider.h"
48
#include "qgsprovidermetadata.h"
49
#include "qgspostgresproviderconnection.h"
50
#include "qgspostgresprovidermetadatautils.h"
51
#include <QRegularExpression>
52

    
53
const QString QgsPostgresProvider::POSTGRES_KEY = QStringLiteral( "postgres" );
54
const QString QgsPostgresProvider::POSTGRES_DESCRIPTION = QStringLiteral( "PostgreSQL/PostGIS data provider" );
55

    
56
static const QString EDITOR_WIDGET_STYLES_TABLE = QStringLiteral( "qgis_editor_widget_styles" );
57

    
58
inline qint64 PKINT2FID( qint32 x )
59
{
60
  return QgsPostgresUtils::int32pk_to_fid( x );
61
}
62

    
63
inline qint32 FID2PKINT( qint64 x )
64
{
65
  return QgsPostgresUtils::fid_to_int32pk( x );
66
}
67

    
68
static bool tableExists( QgsPostgresConn &conn, const QString &name )
69
{
70
  QgsPostgresResult res( conn.LoggedPQexec( QStringLiteral( "tableExists" ), "SELECT EXISTS ( SELECT oid FROM pg_catalog.pg_class WHERE relname=" + QgsPostgresConn::quotedValue( name ) + ")" ) );
71
  return res.PQgetvalue( 0, 0 ).startsWith( 't' );
72
}
73

    
74
static bool columnExists( QgsPostgresConn &conn, const QString &table, const QString &column )
75
{
76
  QgsPostgresResult res( conn.LoggedPQexec( QStringLiteral( "columnExists" ), "SELECT COUNT(*) FROM information_schema.columns WHERE table_name=" + QgsPostgresConn::quotedValue( table ) + " and column_name=" + QgsPostgresConn::quotedValue( column ) ) );
77
  return res.PQgetvalue( 0, 0 ).toInt() > 0;
78
}
79

    
80
QgsPostgresPrimaryKeyType
81
  QgsPostgresProvider::pkType( const QgsField &f ) const
82
{
83
  switch ( f.type() )
84
  {
85
    case QMetaType::Type::LongLong:
86
      // PostgreSQL doesn't have native "unsigned" types.
87
      // Unsigned primary keys are emulated by the serial/bigserial
88
      // pseudo-types, in which autogenerated values are always > 0;
89
      // however, the database accepts manually inserted 0 and negative values
90
      // in these fields.
91
      return PktInt64;
92

    
93
    case QMetaType::Type::Int:
94
      return PktInt;
95

    
96
    default:
97
      return PktFidMap;
98
  }
99
}
100

    
101

    
102
QgsPostgresProvider::QgsPostgresProvider( QString const &uri, const ProviderOptions &options, Qgis::DataProviderReadFlags flags )
103
  : QgsVectorDataProvider( uri, options, flags )
104
  , mShared( new QgsPostgresSharedData )
105
{
106
  QgsDebugMsgLevel( QStringLiteral( "URI: %1 " ).arg( uri ), 2 );
107

    
108
  mUri = QgsDataSourceUri( uri );
109

    
110
  // populate members from the uri structure
111
  mSchemaName = mUri.schema();
112
  mTableName = mUri.table();
113
  mGeometryColumn = mUri.geometryColumn();
114
  mBoundingBoxColumn = mUri.param( "bbox" );
115
  if ( mBoundingBoxColumn.isEmpty() )
116
  {
117
    mBoundingBoxColumn = mGeometryColumn;
118
  }
119
  mSqlWhereClause = mUri.sql();
120
  mRequestedSrid = mUri.srid();
121
  mRequestedGeomType = mUri.wkbType();
122

    
123
  const QString checkUnicityKey { QStringLiteral( "checkPrimaryKeyUnicity" ) };
124
  if ( mUri.hasParam( checkUnicityKey ) )
125
  {
126
    if ( mUri.param( checkUnicityKey ).compare( QLatin1String( "0" ) ) == 0 )
127
    {
128
      mCheckPrimaryKeyUnicity = false;
129
    }
130
    else
131
    {
132
      mCheckPrimaryKeyUnicity = true;
133
    }
134
    if ( mReadFlags & Qgis::DataProviderReadFlag::TrustDataSource )
135
    {
136
      mCheckPrimaryKeyUnicity = false;
137
    }
138
  }
139

    
140
  if ( mSchemaName.isEmpty() && mTableName.startsWith( '(' ) && mTableName.endsWith( ')' ) )
141
  {
142
    mIsQuery = true;
143
    setQuery( mTableName );
144
    mTableName.clear();
145
  }
146
  else
147
  {
148
    mIsQuery = false;
149

    
150
    setQuery( ( !mSchemaName.isEmpty() ? quotedIdentifier( mSchemaName ) + '.' : QString() ) + ( !mTableName.isEmpty() ? quotedIdentifier( mTableName ) : QString() ) );
151
  }
152

    
153
  mUseEstimatedMetadata = mUri.useEstimatedMetadata();
154
  if ( mReadFlags & Qgis::DataProviderReadFlag::TrustDataSource )
155
  {
156
    mUseEstimatedMetadata = true;
157
  }
158
  mSelectAtIdDisabled = mUri.selectAtIdDisabled();
159

    
160
  QgsDebugMsgLevel( QStringLiteral( "Connection info is %1" ).arg( mUri.connectionInfo( false ) ), 2 );
161
  QgsDebugMsgLevel( QStringLiteral( "Geometry column is: %1" ).arg( mGeometryColumn ), 2 );
162
  QgsDebugMsgLevel( QStringLiteral( "Schema is: %1" ).arg( mSchemaName ), 2 );
163
  QgsDebugMsgLevel( QStringLiteral( "Table name is: %1" ).arg( mTableName ), 2 );
164
  QgsDebugMsgLevel( QStringLiteral( "Query is: %1" ).arg( mQuery ), 2 );
165
  QgsDebugMsgLevel( QStringLiteral( "Where clause is: %1" ).arg( mSqlWhereClause ), 2 );
166

    
167
  // no table/query passed, the provider could be used to get tables
168
  if ( mQuery.isEmpty() )
169
  {
170
    return;
171
  }
172

    
173
  mConnectionRO = QgsPostgresConn::connectDb( mUri, true, true, false, !mReadFlags.testFlag( Qgis::DataProviderReadFlag::SkipCredentialsRequest ) );
174
  if ( !mConnectionRO )
175
  {
176
    return;
177
  }
178

    
179
  // if credentials were updated during database connection, update the provider's uri accordingly
180
  if ( !mUri.username().isEmpty() )
181
    mUri.setUsername( mConnectionRO->uri().username() );
182
  if ( !mUri.password().isEmpty() )
183
    mUri.setPassword( mConnectionRO->uri().password() );
184

    
185
  if ( !hasSufficientPermsAndCapabilities() ) // check permissions and set capabilities
186
  {
187
    disconnectDb();
188
    return;
189
  }
190

    
191
  if ( !getGeometryDetails() ) // gets srid, geometry and data type
192
  {
193
    // the table is not a geometry table
194
    QgsMessageLog::logMessage( tr( "Invalid PostgreSQL layer" ), tr( "PostGIS" ) );
195
    disconnectDb();
196
    return;
197
  }
198

    
199
  if ( mDetectedGeomType != Qgis::WkbType::Unknown )
200
  {
201
    // most common case, elevation can be computed via mDetectedGeomType
202
    elevationProperties()->setContainsElevationData( QgsWkbTypes::hasZ( mDetectedGeomType ) );
203
  }
204

    
205
  // NOTE: mValid would be true after true return from
206
  // getGeometryDetails, see https://github.com/qgis/QGIS/issues/21807
207

    
208
  if ( mSpatialColType == SctTopoGeometry )
209
  {
210
    if ( !getTopoLayerInfo() ) // gets topology name and layer id
211
    {
212
      QgsMessageLog::logMessage( tr( "Invalid PostgreSQL topology layer" ), tr( "PostGIS" ) );
213
      mValid = false;
214
      disconnectDb();
215
      return;
216
    }
217
  }
218

    
219
  // Try to load metadata
220
  const QString schemaQuery = QStringLiteral( "SELECT table_schema FROM information_schema.tables WHERE table_name = 'qgis_layer_metadata'" );
221
  QgsPostgresResult res( mConnectionRO->LoggedPQexec( "QgsPostgresProvider", schemaQuery ) );
222
  if ( res.PQntuples() > 0 )
223
  {
224
    const QString schemaName = res.PQgetvalue( 0, 0 );
225
    // TODO: also filter CRS?
226
    const QString selectQuery = QStringLiteral( R"SQL(
227
            SELECT
228
              qmd
229
           FROM %4.qgis_layer_metadata
230
             WHERE
231
                f_table_schema=%1
232
                AND f_table_name=%2
233
                AND f_geometry_column %3
234
                AND layer_type='vector'
235
           )SQL" )
236
                                  .arg( QgsPostgresConn::quotedValue( mUri.schema() ) )
237
                                  .arg( QgsPostgresConn::quotedValue( mUri.table() ) )
238
                                  .arg( mUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( mUri.geometryColumn() ) ) )
239
                                  .arg( QgsPostgresConn::quotedIdentifier( schemaName ) );
240

    
241
    QgsPostgresResult res( mConnectionRO->LoggedPQexec( "QgsPostgresProvider", selectQuery ) );
242
    if ( res.PQntuples() > 0 )
243
    {
244
      QgsLayerMetadata metadata;
245
      QDomDocument doc;
246
      doc.setContent( res.PQgetvalue( 0, 0 ) );
247
      mLayerMetadata.readMetadataXml( doc.documentElement() );
248
    }
249
  }
250

    
251
  // set the primary key
252
  if ( !determinePrimaryKey() )
253
  {
254
    QgsMessageLog::logMessage( tr( "PostgreSQL layer has no primary key." ), tr( "PostGIS" ) );
255
    mValid = false;
256
    disconnectDb();
257
    return;
258
  }
259

    
260
  setNativeTypes( mConnectionRO->nativeTypes() );
261

    
262
  QString key;
263
  switch ( mPrimaryKeyType )
264
  {
265
    case PktOid:
266
      key = QStringLiteral( "oid" );
267
      break;
268
    case PktTid:
269
      key = QStringLiteral( "tid" );
270
      break;
271
    case PktInt:
272
      Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
273
      Q_ASSERT( mPrimaryKeyAttrs[0] >= 0 && mPrimaryKeyAttrs[0] < mAttributeFields.count() );
274
      key = mAttributeFields.at( mPrimaryKeyAttrs.at( 0 ) ).name();
275
      break;
276
    case PktInt64:
277
    case PktUint64:
278
    case PktFidMap:
279
    {
280
      QString delim;
281
      const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
282
      for ( int idx : constMPrimaryKeyAttrs )
283
      {
284
        key += delim + mAttributeFields.at( idx ).name();
285
        delim = ',';
286
      }
287
    }
288
    break;
289
    case PktUnknown:
290
      QgsMessageLog::logMessage( tr( "PostgreSQL layer has unknown primary key type." ), tr( "PostGIS" ) );
291
      mValid = false;
292
      break;
293
  }
294

    
295
  if ( mValid )
296
  {
297
    mUri.setKeyColumn( key );
298
    setDataSourceUri( mUri.uri( false ) );
299
  }
300
  else
301
  {
302
    disconnectDb();
303
  }
304

    
305
  mLayerMetadata.setType( QStringLiteral( "dataset" ) );
306
  mLayerMetadata.setCrs( crs() );
307

    
308
  // Constructor is called in another thread than the thread where the provider will live,
309
  // so we disconnect the DB, connection will be done later in the provider thread when needed
310
  if ( flags.testFlag( Qgis::DataProviderReadFlag::ParallelThreadLoading ) )
311
    disconnectDb();
312
}
313

    
314
QgsPostgresProvider::~QgsPostgresProvider()
315
{
316
  disconnectDb();
317

    
318
  QgsDebugMsgLevel( QStringLiteral( "deconstructing." ), 3 );
319
}
320

    
321

    
322
QgsAbstractFeatureSource *QgsPostgresProvider::featureSource() const
323
{
324
  return new QgsPostgresFeatureSource( this );
325
}
326

    
327
QgsPostgresConn *QgsPostgresProvider::connectionRO() const
328
{
329
  if ( mTransaction )
330
    return mTransaction->connection();
331

    
332
  if ( !mConnectionRO )
333
    mConnectionRO = QgsPostgresConn::connectDb( mUri, true, true, false, !mReadFlags.testFlag( Qgis::DataProviderReadFlag::SkipCredentialsRequest ) );
334

    
335
  return mConnectionRO;
336
}
337

    
338
void QgsPostgresProvider::setListening( bool isListening )
339
{
340
  if ( !mValid )
341
    return;
342

    
343
  if ( isListening && !mListener )
344
  {
345
    mListener.reset( QgsPostgresListener::create( mUri.connectionInfo( false ) ).release() );
346
    connect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify );
347
  }
348
  else if ( !isListening && mListener )
349
  {
350
    disconnect( mListener.get(), &QgsPostgresListener::notify, this, &QgsPostgresProvider::notify );
351
    mListener.reset();
352
  }
353
}
354

    
355
Qgis::VectorLayerTypeFlags QgsPostgresProvider::vectorLayerTypeFlags() const
356
{
357
  Qgis::VectorLayerTypeFlags flags;
358
  if ( mValid && mIsQuery )
359
  {
360
    flags.setFlag( Qgis::VectorLayerTypeFlag::SqlQuery );
361
  }
362
  return flags;
363
}
364

    
365
void QgsPostgresProvider::handlePostCloneOperations( QgsVectorDataProvider *source )
366
{
367
  mShared = qobject_cast<QgsPostgresProvider *>( source )->mShared;
368
}
369

    
370
void QgsPostgresProvider::reloadProviderData()
371
{
372
  mShared->setFeaturesCounted( -1 );
373
  mLayerExtent.reset();
374
}
375

    
376
QgsPostgresConn *QgsPostgresProvider::connectionRW()
377
{
378
  if ( mTransaction )
379
  {
380
    return mTransaction->connection();
381
  }
382
  else if ( !mConnectionRW )
383
  {
384
    mConnectionRW = QgsPostgresConn::connectDb( mUri, false );
385
  }
386
  return mConnectionRW;
387
}
388

    
389
QgsTransaction *QgsPostgresProvider::transaction() const
390
{
391
  return mTransaction;
392
}
393

    
394
QString QgsPostgresProvider::providerKey()
395
{
396
  return POSTGRES_KEY;
397
}
398

    
399
void QgsPostgresProvider::setTransaction( QgsTransaction *transaction )
400
{
401
  // static_cast since layers cannot be added to a transaction of a non-matching provider
402
  mTransaction = static_cast<QgsPostgresTransaction *>( transaction );
403

    
404
  const QString sessionRoleKey = QStringLiteral( "session_role" );
405
  if ( mUri.hasParam( sessionRoleKey ) )
406
  {
407
    const QString sessionRole = mUri.param( sessionRoleKey );
408
    if ( !sessionRole.isEmpty() )
409
    {
410
      if ( !mTransaction->connection()->setSessionRole( sessionRole ) )
411
      {
412
        QgsDebugMsgLevel(
413
          QStringLiteral(
414
            "Set session role failed for ROLE %1"
415
          )
416
            .arg( quotedValue( sessionRole ) ),
417
          2
418
        );
419
      }
420
    }
421
  }
422
}
423

    
424
QgsReferencedGeometry QgsPostgresProvider::fromEwkt( const QString &ewkt, QgsPostgresConn *conn )
425
{
426
  thread_local const QRegularExpression regularExpressionSRID( "^SRID=(\\d+);" );
427

    
428
  QRegularExpressionMatch regularExpressionMatch = regularExpressionSRID.match( ewkt );
429
  if ( !regularExpressionMatch.hasMatch() )
430
  {
431
    QgsGeometry geom = QgsGeometry::fromWkt( ewkt );
432
    QgsCoordinateReferenceSystem crs; // TODO: use projects' crs ?
433
    return QgsReferencedGeometry( geom, crs );
434
  }
435

    
436
  QString wkt = ewkt.mid( regularExpressionMatch.captured( 0 ).size() );
437
  int srid = regularExpressionMatch.captured( 1 ).toInt();
438

    
439

    
440
  QgsGeometry geom = QgsGeometry::fromWkt( wkt );
441
  return QgsReferencedGeometry( geom, sridToCrs( srid, conn ) );
442
}
443

    
444
QString QgsPostgresProvider::toEwkt( const QgsReferencedGeometry &geom, QgsPostgresConn *conn )
445
{
446
  if ( !geom.isNull() )
447
    return QStringLiteral( "SRID=%1;%2" ).arg( QString::number( crsToSrid( geom.crs(), conn ) ), geom.asWkt() );
448
  else
449
    return QString();
450
}
451

    
452
QString QgsPostgresProvider::geomAttrToString( const QVariant &attr, QgsPostgresConn *conn )
453
{
454
  if ( attr.userType() == QMetaType::Type::QString )
455
    return attr.toString();
456
  else
457
    return toEwkt( attr.value<QgsReferencedGeometry>(), conn );
458
}
459

    
460
int QgsPostgresProvider::crsToSrid( const QgsCoordinateReferenceSystem &crs, QgsPostgresConn *conn )
461
{
462
  int srid = -1;
463
  if ( conn )
464
    srid = conn->crsToSrid( crs );
465
  return srid;
466
}
467

    
468
QgsCoordinateReferenceSystem QgsPostgresProvider::sridToCrs( int srid, QgsPostgresConn *conn )
469
{
470
  QgsCoordinateReferenceSystem crs;
471
  if ( conn )
472
    crs = conn->sridToCrs( srid );
473
  return crs;
474
}
475

    
476
void QgsPostgresProvider::disconnectDb()
477
{
478
  if ( mConnectionRO )
479
  {
480
    mConnectionRO->unref();
481
    mConnectionRO = nullptr;
482
  }
483

    
484
  if ( mConnectionRW )
485
  {
486
    mConnectionRW->unref();
487
    mConnectionRW = nullptr;
488
  }
489
}
490

    
491
QString QgsPostgresProvider::quotedByteaValue( const QVariant &value )
492
{
493
  if ( QgsVariantUtils::isNull( value ) )
494
    return QStringLiteral( "NULL" );
495

    
496
  const QByteArray ba = value.toByteArray();
497
  const unsigned char *buf = reinterpret_cast<const unsigned char *>( ba.constData() );
498
  QString param;
499
  param.reserve( ba.length() * 4 );
500
  for ( int i = 0; i < ba.length(); ++i )
501
  {
502
    param += QStringLiteral( "\\%1" ).arg( static_cast<int>( buf[i] ), 3, 8, QChar( '0' ) );
503
  }
504
  return QStringLiteral( "decode('%1','escape')" ).arg( param );
505
}
506

    
507
QString QgsPostgresProvider::storageType() const
508
{
509
  return QStringLiteral( "PostgreSQL database with PostGIS extension" );
510
}
511

    
512
QgsFeatureIterator QgsPostgresProvider::getFeatures( const QgsFeatureRequest &request ) const
513
{
514
  if ( !mValid )
515
  {
516
    QgsMessageLog::logMessage( tr( "Read attempt on an invalid PostgreSQL data source" ), tr( "PostGIS" ) );
517
    return QgsFeatureIterator();
518
  }
519

    
520
  QgsPostgresFeatureSource *featureSrc = static_cast<QgsPostgresFeatureSource *>( featureSource() );
521
  return QgsFeatureIterator( new QgsPostgresFeatureIterator( featureSrc, true, request ) );
522
}
523

    
524

    
525
QString QgsPostgresProvider::pkParamWhereClause( int offset, const char *alias ) const
526
{
527
  QString whereClause;
528

    
529
  QString aliased;
530
  if ( alias )
531
    aliased = QStringLiteral( "%1." ).arg( alias );
532

    
533
  switch ( mPrimaryKeyType )
534
  {
535
    case PktTid:
536
      whereClause = QStringLiteral( "%2ctid=$%1" ).arg( offset ).arg( aliased );
537
      break;
538

    
539
    case PktOid:
540
      whereClause = QStringLiteral( "%2oid=$%1" ).arg( offset ).arg( aliased );
541
      break;
542

    
543
    case PktInt:
544
      Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
545
      whereClause = QStringLiteral( "%3%1=$%2" ).arg( quotedIdentifier( field( mPrimaryKeyAttrs[0] ).name() ) ).arg( offset ).arg( aliased );
546
      break;
547

    
548
    case PktInt64:
549
    case PktUint64:
550
    case PktFidMap:
551
    {
552
      QString delim;
553
      for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ )
554
      {
555
        int idx = mPrimaryKeyAttrs[i];
556
        QgsField fld = field( idx );
557

    
558
        whereClause += delim + QStringLiteral( "%3%1=$%2" ).arg( connectionRO()->fieldExpressionForWhereClause( fld ) ).arg( offset++ ).arg( aliased );
559
        delim = QStringLiteral( " AND " );
560
      }
561
    }
562
    break;
563

    
564
    case PktUnknown:
565
      Q_ASSERT( !"FAILURE: Primary key unknown" );
566
      whereClause = QStringLiteral( "NULL" );
567
      break;
568
  }
569

    
570
  if ( !mSqlWhereClause.isEmpty() )
571
  {
572
    if ( !whereClause.isEmpty() )
573
      whereClause += QLatin1String( " AND " );
574

    
575
    whereClause += '(' + mSqlWhereClause + ')';
576
  }
577

    
578
  return whereClause;
579
}
580

    
581
void QgsPostgresProvider::appendPkParams( QgsFeatureId featureId, QStringList &params ) const
582
{
583
  switch ( mPrimaryKeyType )
584
  {
585
    case PktOid:
586
      params << QString::number( featureId );
587
      break;
588

    
589
    case PktInt:
590
      params << QString::number( FID2PKINT( featureId ) );
591
      break;
592

    
593
    case PktTid:
594
      params << QStringLiteral( "'(%1,%2)'" ).arg( FID_TO_NUMBER( featureId ) >> 16 ).arg( FID_TO_NUMBER( featureId ) & 0xffff );
595
      break;
596

    
597
    case PktInt64:
598
    case PktUint64:
599
    case PktFidMap:
600
    {
601
      QVariantList pkVals = mShared->lookupKey( featureId );
602
      if ( !pkVals.isEmpty() )
603
      {
604
        Q_ASSERT( pkVals.size() == mPrimaryKeyAttrs.size() );
605
      }
606

    
607
      for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ )
608
      {
609
        if ( i < pkVals.size() )
610
        {
611
          params << pkVals[i].toString();
612
        }
613
        else
614
        {
615
          QgsDebugError( QStringLiteral( "FAILURE: Key value %1 for feature %2 not found." ).arg( mPrimaryKeyAttrs[i] ).arg( featureId ) );
616
          params << QStringLiteral( "NULL" );
617
        }
618
      }
619

    
620
      QgsDebugMsgLevel( QStringLiteral( "keys params: %1" ).arg( params.join( "; " ) ), 2 );
621
    }
622
    break;
623

    
624
    case PktUnknown:
625
      Q_ASSERT( !"FAILURE: Primary key unknown" );
626
      break;
627
  }
628
}
629

    
630

    
631
QString QgsPostgresProvider::whereClause( QgsFeatureId featureId ) const
632
{
633
  return QgsPostgresUtils::whereClause( featureId, mAttributeFields, connectionRO(), mPrimaryKeyType, mPrimaryKeyAttrs, mShared );
634
}
635

    
636
QString QgsPostgresProvider::whereClause( QgsFeatureIds featureIds ) const
637
{
638
  return QgsPostgresUtils::whereClause( featureIds, mAttributeFields, connectionRO(), mPrimaryKeyType, mPrimaryKeyAttrs, mShared );
639
}
640

    
641

    
642
QString QgsPostgresUtils::whereClause( QgsFeatureId featureId, const QgsFields &fields, QgsPostgresConn *conn, QgsPostgresPrimaryKeyType pkType, const QList<int> &pkAttrs, const std::shared_ptr<QgsPostgresSharedData> &sharedData )
643
{
644
  QString whereClause;
645

    
646
  switch ( pkType )
647
  {
648
    case PktTid:
649
      whereClause = QStringLiteral( "ctid='(%1,%2)'" )
650
                      .arg( FID_TO_NUMBER( featureId ) >> 16 )
651
                      .arg( FID_TO_NUMBER( featureId ) & 0xffff );
652
      break;
653

    
654
    case PktOid:
655
      whereClause = QStringLiteral( "oid=%1" ).arg( featureId );
656
      break;
657

    
658
    case PktInt:
659
      Q_ASSERT( pkAttrs.size() == 1 );
660
      whereClause = QStringLiteral( "%1=%2" ).arg( QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) ).arg( FID2PKINT( featureId ) );
661
      break;
662

    
663
    case PktInt64:
664
    case PktUint64:
665
    {
666
      Q_ASSERT( pkAttrs.size() == 1 );
667
      QVariantList pkVals = sharedData->lookupKey( featureId );
668
      if ( !pkVals.isEmpty() )
669
      {
670
        QgsField fld = fields.at( pkAttrs[0] );
671
        whereClause = conn->fieldExpression( fld );
672
        if ( !QgsVariantUtils::isNull( pkVals[0] ) )
673
          whereClause += '=' + pkVals[0].toString();
674
        else
675
          whereClause += QLatin1String( " IS NULL" );
676
      }
677
    }
678
    break;
679

    
680
    case PktFidMap:
681
    {
682
      QVariantList pkVals = sharedData->lookupKey( featureId );
683
      if ( !pkVals.isEmpty() )
684
      {
685
        Q_ASSERT( pkVals.size() == pkAttrs.size() );
686

    
687
        QString delim;
688
        for ( int i = 0; i < pkAttrs.size(); i++ )
689
        {
690
          int idx = pkAttrs[i];
691
          QgsField fld = fields.at( idx );
692

    
693
          whereClause += delim + conn->fieldExpressionForWhereClause( fld, static_cast<QMetaType::Type>( pkVals[i].userType() ) );
694
          if ( QgsVariantUtils::isNull( pkVals[i] ) )
695
            whereClause += QLatin1String( " IS NULL" );
696
          else
697
            whereClause += '=' + QgsPostgresConn::quotedValue( pkVals[i] ); // remove toString as it must be handled by quotedValue function
698

    
699
          delim = QStringLiteral( " AND " );
700
        }
701
      }
702
      else
703
      {
704
        QgsDebugError( QStringLiteral( "FAILURE: Key values for feature %1 not found." ).arg( featureId ) );
705
        whereClause = QStringLiteral( "NULL" );
706
      }
707
    }
708
    break;
709

    
710
    case PktUnknown:
711
      Q_ASSERT( !"FAILURE: Primary key unknown" );
712
      whereClause = QStringLiteral( "NULL" );
713
      break;
714
  }
715

    
716
  return whereClause;
717
}
718

    
719
QString QgsPostgresUtils::whereClause( const QgsFeatureIds &featureIds, const QgsFields &fields, QgsPostgresConn *conn, QgsPostgresPrimaryKeyType pkType, const QList<int> &pkAttrs, const std::shared_ptr<QgsPostgresSharedData> &sharedData )
720
{
721
  auto lookupKeyWhereClause = [=] {
722
    if ( featureIds.isEmpty() )
723
      return QString();
724

    
725
    //simple primary key, so prefer to use an "IN (...)" query. These are much faster then multiple chained ...OR... clauses
726
    QString delim;
727
    QString expr = QStringLiteral( "%1 IN (" ).arg( QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) );
728

    
729
    for ( const QgsFeatureId featureId : std::as_const( featureIds ) )
730
    {
731
      const QVariantList pkVals = sharedData->lookupKey( featureId );
732
      if ( !pkVals.isEmpty() )
733
      {
734
        expr += delim + QgsPostgresConn::quotedValue( pkVals.at( 0 ) );
735
        delim = ',';
736
      }
737
    }
738
    expr += ')';
739

    
740
    return expr;
741
  };
742

    
743
  switch ( pkType )
744
  {
745
    case PktOid:
746
    case PktInt:
747
    {
748
      QString expr;
749

    
750
      //simple primary key, so prefer to use an "IN (...)" query. These are much faster then multiple chained ...OR... clauses
751
      if ( !featureIds.isEmpty() )
752
      {
753
        QString delim;
754
        expr = QStringLiteral( "%1 IN (" ).arg( ( pkType == PktOid ? QStringLiteral( "oid" ) : QgsPostgresConn::quotedIdentifier( fields.at( pkAttrs[0] ).name() ) ) );
755

    
756
        for ( const QgsFeatureId featureId : std::as_const( featureIds ) )
757
        {
758
          expr += delim + FID_TO_STRING( ( pkType == PktOid ? featureId : FID2PKINT( featureId ) ) );
759
          delim = ',';
760
        }
761
        expr += ')';
762
      }
763

    
764
      return expr;
765
    }
766
    case PktInt64:
767
    case PktUint64:
768
      return lookupKeyWhereClause();
769

    
770
    case PktFidMap:
771
    case PktTid:
772
    case PktUnknown:
773
    {
774
      // on simple string primary key we can use IN
775
      if ( pkType == PktFidMap && pkAttrs.count() == 1 && fields.at( pkAttrs[0] ).type() == QMetaType::Type::QString )
776
        return lookupKeyWhereClause();
777

    
778
      //complex primary key, need to build up where string
779
      QStringList whereClauses;
780
      for ( const QgsFeatureId featureId : std::as_const( featureIds ) )
781
      {
782
        whereClauses << whereClause( featureId, fields, conn, pkType, pkAttrs, sharedData );
783
      }
784
      return whereClauses.isEmpty() ? QString() : whereClauses.join( QLatin1String( " OR " ) ).prepend( '(' ).append( ')' );
785
    }
786
  }
787
  return QString(); //avoid warning
788
}
789

    
790
QString QgsPostgresUtils::andWhereClauses( const QString &c1, const QString &c2 )
791
{
792
  if ( c1.isEmpty() )
793
    return c2;
794
  if ( c2.isEmpty() )
795
    return c1;
796

    
797
  return QStringLiteral( "(%1) AND (%2)" ).arg( c1, c2 );
798
}
799

    
800
void QgsPostgresUtils::replaceInvalidXmlChars( QString &xml )
801
{
802
  static const QRegularExpression replaceRe { QStringLiteral( "([\\x00-\\x08\\x0B-\\x1F\\x7F])" ) };
803
  QRegularExpressionMatchIterator it { replaceRe.globalMatch( xml ) };
804
  while ( it.hasNext() )
805
  {
806
    const QRegularExpressionMatch match { it.next() };
807
    const QChar c { match.captured( 1 ).at( 0 ) };
808
    xml.replace( c, QStringLiteral( "UTF-8[%1]" ).arg( c.unicode() ) );
809
  }
810
}
811

    
812
void QgsPostgresUtils::restoreInvalidXmlChars( QString &xml )
813
{
814
  static const QRegularExpression replaceRe { QStringLiteral( R"raw(UTF-8\[(\d+)\])raw" ) };
815
  QRegularExpressionMatchIterator it { replaceRe.globalMatch( xml ) };
816
  while ( it.hasNext() )
817
  {
818
    const QRegularExpressionMatch match { it.next() };
819
    bool ok;
820
    const ushort code { match.captured( 1 ).toUShort( &ok ) };
821
    if ( ok )
822
    {
823
      xml.replace( QStringLiteral( "UTF-8[%1]" ).arg( code ), QChar( code ) );
824
    }
825
  }
826
}
827

828
QString QgsPostgresProvider::filterWhereClause() const
829
{
830
  QString where;
831
  QString delim = QStringLiteral( " WHERE " );
832

833
  if ( !mSqlWhereClause.isEmpty() )
834
  {
835
    where += delim + '(' + mSqlWhereClause + ')';
836
    delim = QStringLiteral( " AND " );
837
  }
838

839
  if ( !mRequestedSrid.isEmpty() && ( mRequestedSrid != mDetectedSrid || mRequestedSrid.toInt() == 0 ) )
840
  {
841
    where += delim + QStringLiteral( "%1(%2%3)=%4" ).arg( connectionRO()->majorVersion() < 2 ? "srid" : "st_srid", quotedIdentifier( mGeometryColumn ), mSpatialColType == SctGeography ? "::geography" : "", mRequestedSrid );
842
    delim = QStringLiteral( " AND " );
843
  }
844

845
  if ( mRequestedGeomType != Qgis::WkbType::Unknown && mRequestedGeomType != mDetectedGeomType )
846
  {
847
    where += delim + QgsPostgresConn::postgisTypeFilter( mGeometryColumn, mRequestedGeomType, mSpatialColType == SctGeography );
848
    delim = QStringLiteral( " AND " );
849
  }
850

851
  return where;
852
}
853

854
void QgsPostgresProvider::setExtent( const QgsRectangle &newExtent )
855
{
856
  mLayerExtent.emplace( newExtent );
857
}
858

859
/**
860
 * Returns the feature type
861
 */
862
Qgis::WkbType QgsPostgresProvider::wkbType() const
863
{
864
  return mRequestedGeomType != Qgis::WkbType::Unknown ? mRequestedGeomType : mDetectedGeomType;
865
}
866

867
QgsLayerMetadata QgsPostgresProvider::layerMetadata() const
868
{
869
  return mLayerMetadata;
870
}
871

872
QgsField QgsPostgresProvider::field( int index ) const
873
{
874
  if ( index < 0 || index >= mAttributeFields.count() )
875
  {
876
    QgsMessageLog::logMessage( tr( "FAILURE: Field %1 not found." ).arg( index ), tr( "PostGIS" ) );
877
    throw PGFieldNotFound();
878
  }
879

880
  return mAttributeFields.at( index );
881
}
882

883
QgsFields QgsPostgresProvider::fields() const
884
{
885
  return mAttributeFields;
886
}
887

888
QString QgsPostgresProvider::dataComment() const
889
{
890
  return mDataComment;
891
}
892

893

894
//! \todo XXX Perhaps this should be promoted to QgsDataProvider?
895
QString QgsPostgresProvider::endianString()
896
{
897
  switch ( QgsApplication::endian() )
898
  {
899
    case QgsApplication::NDR:
900
      return QStringLiteral( "NDR" );
901
    case QgsApplication::XDR:
902
      return QStringLiteral( "XDR" );
903
    default:
904
      return QStringLiteral( "Unknown" );
905
  }
906
}
907

908

909
struct PGTypeInfo
910
{
911
    QString typeName;
912
    QString typeType;
913
    QString typeElem;
914
    int typeLen;
915
};
916

917
bool QgsPostgresProvider::loadFields()
918
{
919
  // Clear cached information about enum values support
920
  mShared->clearSupportsEnumValuesCache();
921

922
  QString sql;
923
  QString attroidsFilter;
924

925
  if ( !mIsQuery )
926
  {
927
    QgsDebugMsgLevel( QStringLiteral( "Loading fields for table %1" ).arg( mTableName ), 2 );
928

929
    if ( mLayerMetadata.abstract().isEmpty() )
930
    {
931
      // Get the table description
932
      sql = QStringLiteral( "SELECT description FROM pg_description WHERE objoid=regclass(%1)::oid AND objsubid=0" ).arg( quotedValue( mQuery ) );
933
      QgsPostgresResult tresult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
934

935
      if ( !tresult.result() )
936
      {
937
        throw PGException( tresult );
938
      }
939

940
      if ( tresult.PQntuples() > 0 )
941
      {
942
        mDataComment = tresult.PQgetvalue( 0, 0 );
943
        mLayerMetadata.setAbstract( mDataComment );
944
      }
945
    }
946
  }
947

948
  // Populate the field vector for this layer. The field vector contains
949
  // field name, type, length, and precision (if numeric)
950
  sql = QStringLiteral( "SELECT * FROM %1 LIMIT 0" ).arg( mQuery );
951

952
  QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
953

954
  QMap<Oid, QMap<int, QString>> fmtFieldTypeMap, descrMap, defValMap, identityMap, generatedMap;
955
  QMap<Oid, QMap<int, Oid>> attTypeIdMap;
956
  QMap<Oid, QMap<int, bool>> notNullMap, uniqueMap;
957
  if ( result.PQnfields() > 0 )
958
  {
959
    // Collect attribute oids
960
    QSet<Oid> attroids;
961
    for ( int i = 0; i < result.PQnfields(); i++ )
962
    {
963
      Oid attroid = result.PQftype( i );
964
      attroids.insert( attroid );
965
    }
966

967
    // Collect table oids
968
    QSet<Oid> tableoids;
969
    for ( int i = 0; i < result.PQnfields(); i++ )
970
    {
971
      Oid tableoid = result.PQftable( i );
972
      if ( tableoid > 0 )
973
      {
974
        tableoids.insert( tableoid );
975
      }
976
    }
977

978
    if ( !tableoids.isEmpty() )
979
    {
980
      QStringList tableoidsList;
981
      const auto constTableoids = tableoids;
982
      for ( Oid tableoid : constTableoids )
983
      {
984
        tableoidsList.append( QString::number( tableoid ) );
985
      }
986

987
      QString tableoidsFilter = '(' + tableoidsList.join( QLatin1Char( ',' ) ) + ')';
988

989
      // Collect formatted field types
990
      sql = QStringLiteral(
991
              "SELECT attrelid, attnum, pg_catalog.format_type(atttypid,atttypmod), pg_catalog.col_description(attrelid,attnum), pg_catalog.pg_get_expr(adbin,adrelid), atttypid, attnotnull::int, indisunique::int%1%2"
992
              " FROM pg_attribute"
993
              " LEFT OUTER JOIN pg_attrdef ON attrelid=adrelid AND attnum=adnum"
994

995
              // find unique constraints if present. Text cast required to handle int2vector comparison. Distinct required as multiple unique constraints may exist
996
              " LEFT OUTER JOIN ( SELECT DISTINCT indrelid, indkey, indisunique FROM pg_index WHERE indisunique ) uniq ON attrelid=indrelid AND attnum::text=indkey::text "
997

998
              " WHERE attrelid IN %3"
999
      )
1000
              .arg( connectionRO()->pgVersion() >= 100000 ? QStringLiteral( ", attidentity" ) : QString(), connectionRO()->pgVersion() >= 120000 ? QStringLiteral( ", attgenerated" ) : QString(), tableoidsFilter );
1001

1002
      QgsPostgresResult fmtFieldTypeResult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
1003

1004
      if ( !fmtFieldTypeResult.result() )
1005
      {
1006
        throw PGException( fmtFieldTypeResult );
1007
      }
1008

1009
      for ( int i = 0; i < fmtFieldTypeResult.PQntuples(); ++i )
1010
      {
1011
        Oid attrelid = fmtFieldTypeResult.PQgetvalue( i, 0 ).toUInt();
1012
        int attnum = fmtFieldTypeResult.PQgetvalue( i, 1 ).toInt(); // Int2
1013
        QString formatType = fmtFieldTypeResult.PQgetvalue( i, 2 );
1014
        QString descr = fmtFieldTypeResult.PQgetvalue( i, 3 );
1015
        QString defVal = fmtFieldTypeResult.PQgetvalue( i, 4 );
1016
        Oid attType = fmtFieldTypeResult.PQgetvalue( i, 5 ).toUInt();
1017
        bool attNotNull = fmtFieldTypeResult.PQgetvalue( i, 6 ).toInt();
1018
        bool uniqueConstraint = fmtFieldTypeResult.PQgetvalue( i, 7 ).toInt();
1019
        QString attIdentity = connectionRO()->pgVersion() >= 100000 ? fmtFieldTypeResult.PQgetvalue( i, 8 ) : " ";
1020

1021
        // On PostgreSQL 12, the field pg_attribute.attgenerated is always filled with "s" if the field is generated,
1022
        // with the possibility of other values in future releases. This indicates "STORED" generated fields.
1023
        // The documentation for version 12 indicates that there is a future possibility of supporting virtual
1024
        // generated values, which might make possible to have values other than "s" on pg_attribute.attgenerated,
1025
        // which should be unimportant for QGIS if the user still won't be able to overwrite the column value.
1026
        // See https://www.postgresql.org/docs/12/ddl-generated-columns.html
1027
        QString attGenerated = connectionRO()->pgVersion() >= 120000 ? fmtFieldTypeResult.PQgetvalue( i, 9 ) : "";
1028
        fmtFieldTypeMap[attrelid][attnum] = formatType;
1029
        descrMap[attrelid][attnum] = descr;
1030
        defValMap[attrelid][attnum] = defVal;
1031
        attTypeIdMap[attrelid][attnum] = attType;
1032
        notNullMap[attrelid][attnum] = attNotNull;
1033
        uniqueMap[attrelid][attnum] = uniqueConstraint;
1034
        identityMap[attrelid][attnum] = attIdentity.isEmpty() ? " " : attIdentity;
1035
        generatedMap[attrelid][attnum] = attGenerated.isEmpty() ? QString() : defVal;
1036

1037
        // Also include atttype oid from pg_attribute, because PQnfields only returns basic type for for domains
1038
        attroids.insert( attType );
1039
      }
1040
    }
1041

1042
    // Prepare filter for fetching pg_type info
1043
    if ( !attroids.isEmpty() )
1044
    {
1045
      QStringList attroidsList;
1046
      for ( Oid attroid : std::as_const( attroids ) )
1047
      {
1048
        attroidsList.append( QString::number( attroid ) );
1049
      }
1050
      attroidsFilter = QStringLiteral( "WHERE oid in (%1)" ).arg( attroidsList.join( ',' ) );
1051
    }
1052
  }
1053

1054
  // Collect type info
1055
  sql = QStringLiteral( "SELECT oid,typname,typtype,typelem,typlen FROM pg_type %1" ).arg( attroidsFilter );
1056
  QgsPostgresResult typeResult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
1057

1058
  QMap<Oid, PGTypeInfo> typeMap;
1059
  for ( int i = 0; i < typeResult.PQntuples(); ++i )
1060
  {
1061
    PGTypeInfo typeInfo = {
1062
      /* typeName = */ typeResult.PQgetvalue( i, 1 ),
1063
      /* typeType = */ typeResult.PQgetvalue( i, 2 ),
1064
      /* typeElem = */ typeResult.PQgetvalue( i, 3 ),
1065
      /* typeLen = */ typeResult.PQgetvalue( i, 4 ).toInt()
1066
    };
1067
    typeMap.insert( typeResult.PQgetvalue( i, 0 ).toUInt(), typeInfo );
1068
  }
1069

1070
  QSet<QString> fields;
1071
  mAttributeFields.clear();
1072
  mIdentityFields.clear();
1073
  for ( int i = 0; i < result.PQnfields(); i++ )
1074
  {
1075
    QString fieldName = result.PQfname( i );
1076
    Oid tableoid = result.PQftable( i );
1077
    int attnum = result.PQftablecol( i );
1078
    QString formattedFieldType = fmtFieldTypeMap[tableoid][attnum];
1079
    if ( fieldName == mGeometryColumn )
1080
    {
1081
      if ( mDetectedGeomType == Qgis::WkbType::Unknown )
1082
      {
1083
        // rare case, elevation can not be computed via mDetectedGeomType, we parse the formattedFieldType and look for Z data
1084
        const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( "geometry\\(\\w+\\s*ZM?(,\\d+)?\\)" ), QRegularExpression::CaseInsensitiveOption );
1085
        const QRegularExpressionMatch match = re.match( formattedFieldType );
1086
        elevationProperties()->setContainsElevationData( match.hasMatch() );
1087
      }
1088
      continue;
1089
    }
1090

1091
    Oid fldtyp = result.PQftype( i );
1092
    int fldMod = result.PQfmod( i );
1093
    int fieldPrec = 0;
1094
    Oid atttypid = attTypeIdMap[tableoid][attnum];
1095

1096
    const PGTypeInfo &typeInfo = typeMap.value( fldtyp );
1097
    QString fieldTypeName = typeInfo.typeName;
1098
    QString fieldTType = typeInfo.typeType;
1099
    int fieldSize = typeInfo.typeLen;
1100

1101
    bool isDomain = ( typeMap.value( atttypid ).typeType == QLatin1String( "d" ) );
1102

1103
    QString originalFormattedFieldType = formattedFieldType;
1104
    if ( isDomain )
1105
    {
1106
      // get correct formatted field type for domain
1107
      sql = QStringLiteral( "SELECT format_type(%1, %2)" ).arg( fldtyp ).arg( fldMod );
1108
      QgsPostgresResult fmtFieldModResult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
1109
      if ( fmtFieldModResult.PQntuples() > 0 )
1110
      {
1111
        formattedFieldType = fmtFieldModResult.PQgetvalue( 0, 0 );
1112
      }
1113
    }
1114

1115
    QString fieldComment = descrMap[tableoid][attnum];
1116

1117
    QMetaType::Type fieldType = QMetaType::Type::UnknownType;
1118
    QMetaType::Type fieldSubType = QMetaType::Type::UnknownType;
1119

1120
    if ( fieldTType == QLatin1String( "b" ) )
1121
    {
1122
      bool isArray = fieldTypeName.startsWith( '_' );
1123

1124
      if ( isArray )
1125
        fieldTypeName = fieldTypeName.mid( 1 );
1126

1127
      if ( fieldTypeName == QLatin1String( "int8" ) || fieldTypeName == QLatin1String( "serial8" ) )
1128
      {
1129
        fieldType = QMetaType::Type::LongLong;
1130
        fieldSize = -1;
1131
        fieldPrec = 0;
1132
      }
1133
      else if ( fieldTypeName == QLatin1String( "int2" ) || fieldTypeName == QLatin1String( "int4" ) || fieldTypeName == QLatin1String( "oid" ) || fieldTypeName == QLatin1String( "serial" ) )
1134
      {
1135
        fieldType = QMetaType::Type::Int;
1136
        fieldSize = -1;
1137
        fieldPrec = 0;
1138
      }
1139
      else if ( fieldTypeName == QLatin1String( "real" ) || fieldTypeName == QLatin1String( "double precision" ) || fieldTypeName == QLatin1String( "float4" ) || fieldTypeName == QLatin1String( "float8" ) )
1140
      {
1141
        fieldType = QMetaType::Type::Double;
1142
        fieldSize = -1;
1143
        fieldPrec = 0;
1144
      }
1145
      else if ( fieldTypeName == QLatin1String( "numeric" ) )
1146
      {
1147
        fieldType = QMetaType::Type::Double;
1148

1149
        if ( formattedFieldType == QLatin1String( "numeric" ) || formattedFieldType.isEmpty() )
1150
        {
1151
          fieldSize = -1;
1152
          fieldPrec = 0;
1153
        }
1154
        else
1155
        {
1156
          const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( QStringLiteral( "numeric\\((\\d+),(\\d+)\\)" ) ) );
1157
          const QRegularExpressionMatch match = re.match( formattedFieldType );
1158
          if ( match.hasMatch() )
1159
          {
1160
            fieldSize = match.captured( 1 ).toInt();
1161
            fieldPrec = match.captured( 2 ).toInt();
1162
          }
1163
          else if ( formattedFieldType != QLatin1String( "numeric" ) )
1164
          {
1165
            QgsMessageLog::logMessage( tr( "Unexpected formatted field type '%1' for field %2" ).arg( formattedFieldType, fieldName ), tr( "PostGIS" ) );
1166
            fieldSize = -1;
1167
            fieldPrec = 0;
1168
          }
1169
        }
1170
      }
1171
      else if ( fieldTypeName == QLatin1String( "money" ) )
1172
      {
1173
        fieldType = QMetaType::Type::Double;
1174
        fieldSize = -1;
1175
        fieldPrec = 2;
1176
      }
1177
      else if ( fieldTypeName == QLatin1String( "varchar" ) )
1178
      {
1179
        fieldType = QMetaType::Type::QString;
1180

1181
        const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( "character varying\\((\\d+)\\)" ) );
1182
        const QRegularExpressionMatch match = re.match( formattedFieldType );
1183
        if ( match.hasMatch() )
1184
        {
1185
          fieldSize = match.captured( 1 ).toInt();
1186
        }
1187
        else
1188
        {
1189
          fieldSize = -1;
1190
        }
1191
      }
1192
      else if ( fieldTypeName == QLatin1String( "date" ) )
1193
      {
1194
        fieldType = QMetaType::Type::QDate;
1195
        fieldSize = -1;
1196
      }
1197
      else if ( fieldTypeName == QLatin1String( "time" ) )
1198
      {
1199
        fieldType = QMetaType::Type::QTime;
1200
        fieldSize = -1;
1201
      }
1202
      else if ( fieldTypeName == QLatin1String( "timestamp" ) || fieldTypeName == QLatin1String( "timestamptz" ) )
1203
      {
1204
        fieldType = QMetaType::Type::QDateTime;
1205
        fieldSize = -1;
1206
      }
1207
      else if ( fieldTypeName == QLatin1String( "bytea" ) )
1208
      {
1209
        fieldType = QMetaType::Type::QByteArray;
1210
        fieldSize = -1;
1211
      }
1212
      else if ( fieldTypeName == QLatin1String( "text" ) || fieldTypeName == QLatin1String( "citext" ) || fieldTypeName == QLatin1String( "geography" ) || fieldTypeName == QLatin1String( "inet" ) || fieldTypeName == QLatin1String( "cidr" ) || fieldTypeName == QLatin1String( "macaddr" ) || fieldTypeName == QLatin1String( "macaddr8" ) || fieldTypeName == QLatin1String( "ltree" ) || fieldTypeName == QLatin1String( "uuid" ) || fieldTypeName == QLatin1String( "xml" ) || fieldTypeName == QLatin1String( "bit" ) || fieldTypeName == QLatin1String( "varbit" ) || fieldTypeName.startsWith( QLatin1String( "time" ) ) || fieldTypeName.startsWith( QLatin1String( "date" ) ) )
1213
      {
1214
        fieldType = QMetaType::Type::QString;
1215
        fieldSize = -1;
1216
      }
1217
      else if ( fieldTypeName == QLatin1String( "geometry" ) )
1218
      {
1219
        fieldType = QMetaType::Type::User;
1220
        fieldSize = -1;
1221
      }
1222
      else if ( fieldTypeName == QLatin1String( "bpchar" ) )
1223
      {
1224
        // although postgres internally uses "bpchar", this is exposed to users as character in postgres
1225
        fieldTypeName = QStringLiteral( "character" );
1226

1227
        fieldType = QMetaType::Type::QString;
1228

1229
        const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( "character\\((\\d+)\\)" ) );
1230
        const QRegularExpressionMatch match = re.match( formattedFieldType );
1231
        if ( match.hasMatch() )
1232
        {
1233
          fieldSize = match.captured( 1 ).toInt();
1234
        }
1235
        else
1236
        {
1237
          QgsDebugError( QStringLiteral( "Unexpected formatted field type '%1' for field %2" )
1238
                           .arg( formattedFieldType, fieldName ) );
1239
          fieldSize = -1;
1240
          fieldPrec = 0;
1241
        }
1242
      }
1243
      else if ( fieldTypeName == QLatin1String( "char" ) )
1244
      {
1245
        fieldType = QMetaType::Type::QString;
1246

1247
        const thread_local QRegularExpression re( QRegularExpression::anchoredPattern( QStringLiteral( "char\\((\\d+)\\)" ) ) );
1248
        const QRegularExpressionMatch match = re.match( formattedFieldType );
1249
        if ( match.hasMatch() )
1250
        {
1251
          fieldSize = match.captured( 1 ).toInt();
1252
        }
1253
        else
1254
        {
1255
          QgsMessageLog::logMessage( tr( "Unexpected formatted field type '%1' for field %2" )
1256
                                       .arg( formattedFieldType, fieldName ) );
1257
          fieldSize = -1;
1258
          fieldPrec = 0;
1259
        }
1260
      }
1261
      else if ( fieldTypeName == QLatin1String( "hstore" ) || fieldTypeName == QLatin1String( "json" ) || fieldTypeName == QLatin1String( "jsonb" ) )
1262
      {
1263
        fieldType = QMetaType::Type::QVariantMap;
1264
        fieldSubType = QMetaType::Type::QString;
1265
        fieldSize = -1;
1266
      }
1267
      else if ( fieldTypeName == QLatin1String( "bool" ) )
1268
      {
1269
        // enum
1270
        fieldType = QMetaType::Type::Bool;
1271
        fieldSize = -1;
1272
      }
1273
      // PG 12 returns "name" type for some system table fields (e.g. information_schema.tables)
1274
      else if ( fieldTypeName == QLatin1String( "name" ) )
1275
      {
1276
        fieldType = QMetaType::Type::QString;
1277
        fieldSize = 63;
1278
      }
1279
      else
1280
      {
1281
        // be tolerant in case of views: this might be a field used as a key
1282
        const Qgis::PostgresRelKind type = relkind();
1283
        if ( ( type == Qgis::PostgresRelKind::View || type == Qgis::PostgresRelKind::MaterializedView ) && parseUriKey( mUri.keyColumn() ).contains( fieldName ) )
1284
        {
1285
          // Assume it is convertible to text
1286
          fieldType = QMetaType::Type::QString;
1287
          fieldSize = -1;
1288
        }
1289
        else if ( fieldTypeName == QLatin1String( "unknown" ) )
1290
        {
1291
          // Assume it is convertible to text
1292
          fieldType = QMetaType::Type::QString;
1293
          fieldSize = -1;
1294
        }
1295
        else
1296
        {
1297
          QgsMessageLog::logMessage( tr( "Field %1 ignored, because of unsupported type %2" ).arg( fieldName, fieldTypeName ), tr( "PostGIS" ) );
1298
          continue;
1299
        }
1300
      }
1301

1302
      if ( isArray )
1303
      {
1304
        fieldTypeName = '_' + fieldTypeName;
1305
        fieldSubType = fieldType;
1306
        fieldType = ( fieldType == QMetaType::Type::QString ? QMetaType::Type::QStringList : QMetaType::Type::QVariantList );
1307
        fieldSize = -1;
1308
      }
1309
    }
1310
    else if ( fieldTType == QLatin1String( "e" ) )
1311
    {
1312
      // enum
1313
      fieldType = QMetaType::Type::QString;
1314
      fieldSize = -1;
1315
    }
1316
    else
1317
    {
1318
      QgsMessageLog::logMessage( tr( "Field %1 ignored, because of unsupported type %2" ).arg( fieldName, fieldTType ), tr( "PostGIS" ) );
1319
      continue;
1320
    }
1321

1322
    if ( fields.contains( fieldName ) )
1323
    {
1324
      QgsMessageLog::logMessage( tr( "Duplicate field %1 found\n" ).arg( fieldName ), tr( "PostGIS" ) );
1325
      // In case of read-only query layers we can safely ignore the issue and rename the duplicated field
1326
      if ( !mIsQuery )
1327
      {
1328
        return false;
1329
      }
1330
      else
1331
      {
1332
        unsigned short int i = 1;
1333
        while ( i < std::numeric_limits<unsigned short int>::max() )
1334
        {
1335
          const QString newName { QStringLiteral( "%1 (%2)" ).arg( fieldName ).arg( ++i ) };
1336
          if ( !fields.contains( newName ) )
1337
          {
1338
            fieldName = newName;
1339
            break;
1340
          }
1341
        }
1342
      }
1343
    }
1344

1345
    fields << fieldName;
1346

1347
    if ( isDomain )
1348
    {
1349
      //field was defined using domain, so use domain type name for fieldTypeName
1350
      fieldTypeName = originalFormattedFieldType;
1351
    }
1352

1353
    // If this is an identity field with constraints and there is no default, let's look for a sequence:
1354
    // we might have a default value created by a sequence named <table>_<field>_seq
1355
    if ( !identityMap[tableoid][attnum].isEmpty()
1356
         && notNullMap[tableoid][attnum]
1357
         && uniqueMap[tableoid][attnum]
1358
         && defValMap[tableoid][attnum].isEmpty() )
1359
    {
1360
      const QString seqSql = QStringLiteral( "SELECT pg_get_serial_sequence(%1, %2)" )
1361
                               .arg( quotedValue( mQuery ) )
1362
                               .arg( quotedValue( fieldName ) );
1363
      QgsPostgresResult seqResult( connectionRO()->PQexec( seqSql ) );
1364
      if ( seqResult.PQntuples() == 1 && !seqResult.PQgetisnull( 0, 0 ) )
1365
      {
1366
        defValMap[tableoid][attnum] = QStringLiteral( "nextval(%1)" ).arg( quotedValue( seqResult.PQgetvalue( 0, 0 ) ) );
1367
      }
1368
    }
1369

1370
    mDefaultValues.insert( mAttributeFields.size(), defValMap[tableoid][attnum] );
1371

1372
    const QString generatedValue = generatedMap[tableoid][attnum];
1373
    if ( !generatedValue.isNull() )
1374
      mGeneratedValues.insert( mAttributeFields.size(), generatedValue );
1375

1376
    QgsField newField = QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment, fieldSubType );
1377
    newField.setReadOnly( !generatedValue.isNull() );
1378

1379
    QgsFieldConstraints constraints;
1380
    if ( notNullMap[tableoid][attnum] || ( mPrimaryKeyAttrs.size() == 1 && mPrimaryKeyAttrs[0] == i ) || identityMap[tableoid][attnum] != ' ' )
1381
      constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
1382
    if ( uniqueMap[tableoid][attnum] || ( mPrimaryKeyAttrs.size() == 1 && mPrimaryKeyAttrs[0] == i ) || identityMap[tableoid][attnum] != ' ' )
1383
      constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
1384
    newField.setConstraints( constraints );
1385

1386
    mIdentityFields.insert( mAttributeFields.size(), identityMap[tableoid][attnum][0].toLatin1() );
1387
    mAttributeFields.append( newField );
1388

1389
    // if we know for sure that this field is not enumerated type or a domain type, let's
1390
    // mark it here, so that enumValues() does not need to check it again (for types like int, text, ...)
1391
    if ( fieldTType != QLatin1String( "e" ) && !isDomain )
1392
      mShared->setFieldSupportsEnumValues( fields.count() - 1, false );
1393
  }
1394

1395
  setEditorWidgets();
1396

1397
  return true;
1398
}
1399

1400
void QgsPostgresProvider::setEditorWidgets()
1401
{
1402
  if ( !tableExists( *connectionRO(), EDITOR_WIDGET_STYLES_TABLE ) )
1403
  {
1404
    return;
1405
  }
1406

1407
  QStringList quotedFnames;
1408
  const QStringList fieldNames = mAttributeFields.names();
1409
  for ( const QString &name : fieldNames )
1410
  {
1411
    quotedFnames << quotedValue( name );
1412
  }
1413

1414
  // We expect the table to be created like this:
1415
  //
1416
  // CREATE TABLE qgis_editor_widget_styles (schema_name TEXT NOT NULL, table_name TEXT NOT NULL, field_name TEXT NOT NULL,
1417
  //                                         type TEXT NOT NULL, config TEXT,
1418
  //                                         PRIMARY KEY(schema_name, table_name, field_name));
1419
  const QString sql = QStringLiteral( "SELECT field_name, type, config "
1420
                                      "FROM %1 WHERE schema_name = %2 "
1421
                                      "AND table_name = %3 "
1422
                                      "AND field_name IN ( %4 )" )
1423
                        .arg( EDITOR_WIDGET_STYLES_TABLE, quotedValue( mSchemaName ), quotedValue( mTableName ), quotedFnames.join( "," ) );
1424
  QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
1425
  for ( int i = 0; i < result.PQntuples(); ++i )
1426
  {
1427
    if ( result.PQgetisnull( i, 2 ) )
1428
      continue; // config can be null and it's OK
1429

1430
    const QString &configTxt = result.PQgetvalue( i, 2 );
1431
    const QString &type = result.PQgetvalue( i, 1 );
1432
    const QString &fname = result.PQgetvalue( i, 0 );
1433
    QVariantMap config;
1434
    QDomDocument doc;
1435
    if ( doc.setContent( configTxt ) )
1436
    {
1437
      config = QgsXmlUtils::readVariant( doc.documentElement() ).toMap();
1438
    }
1439
    else
1440
    {
1441
      QgsMessageLog::logMessage(
1442
        tr( "Cannot parse widget configuration for field %1.%2.%3\n" )
1443
          .arg( mSchemaName, mTableName, fname ),
1444
        tr( "PostGIS" )
1445
      );
1446
      continue;
1447
    }
1448

1449
    // Set corresponding editor widget
1450
    for ( auto &field : mAttributeFields )
1451
    {
1452
      if ( field.name() == fname )
1453
      {
1454
        field.setEditorWidgetSetup( QgsEditorWidgetSetup( type, config ) );
1455
        break;
1456
      }
1457
    }
1458
  }
1459
}
1460

1461
bool QgsPostgresProvider::hasSufficientPermsAndCapabilities()
1462
{
1463
  QgsDebugMsgLevel( QStringLiteral( "Checking for permissions on the relation" ), 2 );
1464

1465
  mEnabledCapabilities = Qgis::VectorProviderCapability::ReloadData;
1466

1467
  QgsPostgresResult testAccess;
1468

1469
  bool forceReadOnly = ( mReadFlags & Qgis::DataProviderReadFlag::ForceReadOnly );
1470
  bool inRecovery = false;
1471

1472
  if ( !mIsQuery )
1473
  {
1474
    // postgres has fast access to features at id (thanks to primary key / unique index)
1475
    // the latter flag is here just for compatibility
1476
    if ( !mSelectAtIdDisabled )
1477
    {
1478
      mEnabledCapabilities |= Qgis::VectorProviderCapability::SelectAtId;
1479
    }
1480

1481
    QString sql = QStringLiteral(
1482
                    "SELECT "
1483
                    "has_table_privilege(%1,'SELECT')," // 0 (select priv)
1484
                    "pg_is_in_recovery(),"              // 1 (in recovery)
1485
                    "current_schema() "                 // 2 (current schema)
1486
    )
1487
                    .arg( quotedValue( mQuery ) );
1488

1489

1490
    if ( connectionRO()->pgVersion() >= 80400 )
1491
    {
1492
      sql += QString(
1493
               ",has_any_column_privilege(%1,'INSERT')" // 3 (insert priv)
1494
               ",has_table_privilege(%1,'DELETE')"      // 4 (delete priv)
1495
               ",has_any_column_privilege(%1,'UPDATE')" // 5 (update priv)
1496
               ",%2"                                    // 6 (geom upd priv)
1497
      )
1498
               .arg( quotedValue( mQuery ), mGeometryColumn.isNull() ? QStringLiteral( "'f'" ) : QStringLiteral( "has_column_privilege(%1,%2,'UPDATE')" ).arg( quotedValue( mQuery ), quotedValue( mGeometryColumn ) ) );
1499
    }
1500
    else
1501
    {
1502
      sql += QString(
1503
               ",has_table_privilege(%1,'INSERT')" // 3 (insert priv)
1504
               ",has_table_privilege(%1,'DELETE')" // 4 (delete priv)
1505
               ",has_table_privilege(%1,'UPDATE')" // 5 (update priv)
1506
               ",has_table_privilege(%1,'UPDATE')" // 6 (geom col priv)
1507
      )
1508
               .arg( quotedValue( mQuery ) );
1509
    }
1510

1511
    testAccess = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
1512
    if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK )
1513
    {
1514
      QgsMessageLog::logMessage( tr( "Unable to determine table access privileges for the %1 relation.\nThe error message from the database was:\n%2.\nSQL: %3" ).arg( mQuery, testAccess.PQresultErrorMessage(), sql ), tr( "PostGIS" ) );
1515
      return false;
1516
    }
1517

1518
    if ( testAccess.PQgetvalue( 0, 0 ) != QLatin1String( "t" ) )
1519
    {
1520
      // SELECT
1521
      QgsMessageLog::logMessage( tr( "User has no SELECT privilege on %1 relation." ).arg( mQuery ), tr( "PostGIS" ) );
1522
      return false;
1523
    }
1524

1525
    if ( testAccess.PQgetvalue( 0, 1 ) == QLatin1String( "t" ) )
1526
    {
1527
      // RECOVERY
1528
      QgsMessageLog::logMessage(
1529
        tr( "PostgreSQL is still in recovery after a database crash\n(or you are connected to a (read-only) standby server).\nWrite accesses will be denied." ),
1530
        tr( "PostGIS" )
1531
      );
1532
      inRecovery = true;
1533
    }
1534

1535
    // CURRENT SCHEMA
1536
    if ( mSchemaName.isEmpty() )
1537
      mSchemaName = testAccess.PQgetvalue( 0, 2 );
1538

1539
    // Do not set editable capabilities if the provider has been forced to be
1540
    // in read-only mode or if the database is still in recovery
1541
    if ( !forceReadOnly && !inRecovery )
1542
    {
1543
      if ( testAccess.PQgetvalue( 0, 3 ) == QLatin1String( "t" ) )
1544
      {
1545
        // INSERT
1546
        mEnabledCapabilities |= Qgis::VectorProviderCapability::AddFeatures;
1547
      }
1548

1549
      if ( testAccess.PQgetvalue( 0, 4 ) == QLatin1String( "t" ) )
1550
      {
1551
        // DELETE
1552
        mEnabledCapabilities |= Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::FastTruncate;
1553
      }
1554

1555
      if ( testAccess.PQgetvalue( 0, 5 ) == QLatin1String( "t" ) )
1556
      {
1557
        // UPDATE
1558
        mEnabledCapabilities |= Qgis::VectorProviderCapability::ChangeAttributeValues;
1559
      }
1560

1561
      if ( testAccess.PQgetvalue( 0, 6 ) == QLatin1String( "t" ) )
1562
      {
1563
        // UPDATE (geom column specific)
1564
        mEnabledCapabilities |= Qgis::VectorProviderCapability::ChangeGeometries;
1565
      }
1566

1567
      // TODO: merge this with the previous query
1568
      sql = QString( "SELECT 1 FROM pg_class,pg_namespace WHERE "
1569
                     "pg_class.relnamespace=pg_namespace.oid AND "
1570
                     "%3 AND "
1571
                     "relname=%1 AND nspname=%2" )
1572
              .arg( quotedValue( mTableName ), quotedValue( mSchemaName ), connectionRO()->pgVersion() < 80100 ? "pg_get_userbyid(relowner)=current_user" : "pg_has_role(relowner,'MEMBER')" );
1573
      testAccess = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
1574
      if ( testAccess.PQresultStatus() == PGRES_TUPLES_OK && testAccess.PQntuples() == 1 )
1575
      {
1576
        mEnabledCapabilities |= Qgis::VectorProviderCapability::AddAttributes | Qgis::VectorProviderCapability::DeleteAttributes | Qgis::VectorProviderCapability::RenameAttributes;
1577
      }
1578
    }
1579
  }
1580
  else
1581
  {
1582
    // Check if the sql is a select query
1583
    if ( !mQuery.startsWith( '(' ) && !mQuery.endsWith( ')' ) )
1584
    {
1585
      QgsMessageLog::logMessage( tr( "The custom query is not a select query." ), tr( "PostGIS" ) );
1586
      return false;
1587
    }
1588

1589
    // get a new alias for the subquery
1590
    int index = 0;
1591
    QString alias;
1592
    QRegularExpression regex;
1593
    do
1594
    {
1595
      alias = QStringLiteral( "subQuery_%1" ).arg( QString::number( index++ ) );
1596
      QString pattern = QStringLiteral( "(\\\"?)%1\\1" ).arg( QgsStringUtils::qRegExpEscape( alias ) );
1597
      regex.setPattern( pattern );
1598
      regex.setPatternOptions( QRegularExpression::CaseInsensitiveOption );
1599
    } while ( mQuery.contains( regex ) );
1600

    
1601
    // convert the custom query into a subquery
1602
    setQuery( QStringLiteral( "%1 AS %2" )
1603
                .arg( mQuery, quotedIdentifier( alias ) ) );
1604

    
1605
    QString sql = QStringLiteral( "SELECT * FROM %1 LIMIT 1" ).arg( mQuery );
1606

    
1607
    testAccess = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
1608
    if ( testAccess.PQresultStatus() != PGRES_TUPLES_OK )
1609
    {
1610
      QgsMessageLog::logMessage( tr( "Unable to execute the query.\nThe error message from the database was:\n%1.\nSQL: %2" ).arg( testAccess.PQresultErrorMessage(), sql ), tr( "PostGIS" ) );
1611
      return false;
1612
    }
1613

    
1614
    if ( !mSelectAtIdDisabled )
1615
    {
1616
      mEnabledCapabilities = Qgis::VectorProviderCapability::SelectAtId;
1617
    }
1618
  }
1619

    
1620
  // supports geometry simplification on provider side
1621
  mEnabledCapabilities |= ( Qgis::VectorProviderCapability::SimplifyGeometries | Qgis::VectorProviderCapability::SimplifyGeometriesWithTopologicalValidation );
1622

    
1623
  //supports transactions
1624
  mEnabledCapabilities |= Qgis::VectorProviderCapability::TransactionSupport;
1625

    
1626
  // supports circular geometries
1627
  mEnabledCapabilities |= Qgis::VectorProviderCapability::CircularGeometries;
1628

    
1629
  // supports layer metadata
1630
  mEnabledCapabilities |= Qgis::VectorProviderCapability::ReadLayerMetadata;
1631

    
1632
  if ( ( mEnabledCapabilities & Qgis::VectorProviderCapability::ChangeGeometries ) && ( mEnabledCapabilities & Qgis::VectorProviderCapability::ChangeAttributeValues ) && mSpatialColType != SctTopoGeometry )
1633
  {
1634
    mEnabledCapabilities |= Qgis::VectorProviderCapability::ChangeFeatures;
1635
  }
1636

    
1637
  return true;
1638
}
1639

    
1640
bool QgsPostgresProvider::determinePrimaryKey()
1641
{
1642
  if ( !loadFields() )
1643
  {
1644
    return false;
1645
  }
1646

    
1647
  // check to see if there is an unique index on the relation, which
1648
  // can be used as a key into the table. Primary keys are always
1649
  // unique indices, so we catch them as well.
1650

    
1651
  QString sql;
1652
  if ( !mIsQuery )
1653
  {
1654
    sql = QStringLiteral( "SELECT count(*) FROM pg_inherits WHERE inhparent=%1::regclass" ).arg( quotedValue( mQuery ) );
1655
    QgsDebugMsgLevel( QStringLiteral( "Checking whether %1 is a parent table" ).arg( sql ), 2 );
1656
    QgsPostgresResult res( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
1657
    bool isParentTable( res.PQntuples() == 0 || res.PQgetvalue( 0, 0 ).toInt() > 0 );
1658

    
1659
    sql = QStringLiteral( "SELECT indexrelid FROM pg_index WHERE indrelid=%1::regclass AND (indisprimary OR indisunique) ORDER BY CASE WHEN indisprimary THEN 1 ELSE 2 END LIMIT 1" ).arg( quotedValue( mQuery ) );
1660
    QgsDebugMsgLevel( QStringLiteral( "Retrieving first primary or unique index: %1" ).arg( sql ), 2 );
1661

    
1662
    res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
1663
    QgsDebugMsgLevel( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ), 2 );
1664

    
1665
    // no primary or unique indices found
1666
    if ( res.PQntuples() == 0 )
1667
    {
1668
      QgsDebugMsgLevel( QStringLiteral( "Relation has no primary key -- investigating alternatives" ), 2 );
1669

    
1670
      // Two options here. If the relation is a table, see if there is
1671
      // an oid column that can be used instead.
1672
      // If the relation is a view try to find a suitable column to use as
1673
      // the primary key.
1674

    
1675
      const Qgis::PostgresRelKind type = relkind();
1676

    
1677
      if ( type == Qgis::PostgresRelKind::OrdinaryTable || type == Qgis::PostgresRelKind::PartitionedTable )
1678
      {
1679
        QgsDebugMsgLevel( QStringLiteral( "Relation is a table. Checking to see if it has an oid column." ), 2 );
1680

    
1681
        mPrimaryKeyAttrs.clear();
1682
        mPrimaryKeyType = PktUnknown;
1683

    
1684
        if ( connectionRO()->pgVersion() >= 100000 )
1685
        {
1686
          // If there is an generated id on the table, use that instead,
1687
          sql = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attidentity IN ('a','d') AND attrelid=regclass(%1) LIMIT 1" ).arg( quotedValue( mQuery ) );
1688
          res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
1689
          if ( res.PQntuples() == 1 )
1690
          {
1691
            // Could warn the user here that performance will suffer if
1692
            // attribute isn't indexed (and that they may want to add a
1693
            // primary key to the table)
1694
            int idx = fieldNameIndex( res.PQgetvalue( 0, 0 ) );
1695
            mPrimaryKeyType = pkType( mAttributeFields.at( idx ) );
1696
            mPrimaryKeyAttrs << idx;
1697
          }
1698
        }
1699

    
1700
        if ( mPrimaryKeyType == PktUnknown )
1701
        {
1702
          // If there is an oid on the table, use that instead,
1703
          sql = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attname='oid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) );
1704

    
1705
          res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
1706
          if ( res.PQntuples() == 1 )
1707
          {
1708
            // Could warn the user here that performance will suffer if
1709
            // oid isn't indexed (and that they may want to add a
1710
            // primary key to the table)
1711
            mPrimaryKeyType = PktOid;
1712
          }
1713
        }
1714

    
1715
        if ( mPrimaryKeyType == PktUnknown )
1716
        {
1717
          sql = QStringLiteral( "SELECT attname FROM pg_attribute WHERE attname='ctid' AND attrelid=regclass(%1)" ).arg( quotedValue( mQuery ) );
1718

    
1719
          res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
1720
          if ( res.PQntuples() == 1 )
1721
          {
1722
            mPrimaryKeyType = PktTid;
1723

    
1724
            QgsMessageLog::logMessage( tr( "Primary key is ctid - changing of existing features disabled (%1; %2)" ).arg( mGeometryColumn, mQuery ) );
1725
            mEnabledCapabilities &= ~( Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::ChangeAttributeValues | Qgis::VectorProviderCapability::ChangeGeometries | Qgis::VectorProviderCapability::ChangeFeatures );
1726
          }
1727
        }
1728

    
1729
        if ( mPrimaryKeyType == PktUnknown )
1730
        {
1731
          QgsMessageLog::logMessage( tr( "The table has no column suitable for use as a key. QGIS requires a primary key, a PostgreSQL oid column or a ctid for tables." ), tr( "PostGIS" ) );
1732
        }
1733
      }
1734
      else if ( type == Qgis::PostgresRelKind::View || type == Qgis::PostgresRelKind::MaterializedView || type == Qgis::PostgresRelKind::ForeignTable )
1735
      {
1736
        determinePrimaryKeyFromUriKeyColumn();
1737
      }
1738
      else
1739
      {
1740
        QgsMessageLog::logMessage( tr( "Unexpected relation type '%1'." ).arg( qgsEnumValueToKey( type ) ), tr( "PostGIS" ) );
1741
      }
1742
    }
1743
    else
1744
    {
1745
      // have a primary key or unique index
1746
      QString indrelid = res.PQgetvalue( 0, 0 );
1747
      sql = QStringLiteral( "SELECT attname,attnotnull FROM pg_index,pg_attribute WHERE indexrelid=%1 AND indrelid=attrelid AND pg_attribute.attnum=any(pg_index.indkey)" ).arg( indrelid );
1748

    
1749
      QgsDebugMsgLevel( "Retrieving key columns: " + sql, 2 );
1750
      res = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
1751
      QgsDebugMsgLevel( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ), 2 );
1752

    
1753
      bool mightBeNull = false;
1754
      QString primaryKey;
1755
      QString delim;
1756

    
1757
      mPrimaryKeyType = PktFidMap; // map by default, will downgrade if needed
1758
      for ( int i = 0; i < res.PQntuples(); i++ )
1759
      {
1760
        QString name = res.PQgetvalue( i, 0 );
1761
        if ( res.PQgetvalue( i, 1 ).startsWith( 'f' ) )
1762
        {
1763
          QgsMessageLog::logMessage( tr( "Unique column '%1' doesn't have a NOT NULL constraint." ).arg( name ), tr( "PostGIS" ) );
1764
          mightBeNull = true;
1765
        }
1766

    
1767
        primaryKey += delim + quotedIdentifier( name );
1768
        delim = ',';
1769

    
1770
        int idx = fieldNameIndex( name );
1771
        if ( idx == -1 )
1772
        {
1773
          QgsDebugMsgLevel( "Skipping " + name, 2 );
1774
          continue;
1775
        }
1776
        QgsField fld = mAttributeFields.at( idx );
1777

    
1778
        // Always use PktFidMap for multi-field keys
1779
        mPrimaryKeyType = i ? PktFidMap : pkType( fld );
1780

    
1781
        mPrimaryKeyAttrs << idx;
1782
      }
1783

    
1784
      if ( ( mightBeNull || isParentTable ) && !mUseEstimatedMetadata && !uniqueData( primaryKey ) )
1785
      {
1786
        QgsMessageLog::logMessage( tr( "Ignoring key candidate because of NULL values or inheritance" ), tr( "PostGIS" ) );
1787
        mPrimaryKeyType = PktUnknown;
1788
        mPrimaryKeyAttrs.clear();
1789
      }
1790
    }
1791
  }
1792
  else
1793
  {
1794
    determinePrimaryKeyFromUriKeyColumn();
1795
  }
1796

    
1797
  if ( mPrimaryKeyAttrs.size() == 1 )
1798
  {
1799
    //primary keys are unique, not null
1800
    QgsFieldConstraints constraints = mAttributeFields.at( mPrimaryKeyAttrs[0] ).constraints();
1801
    constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
1802
    constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
1803
    mAttributeFields[mPrimaryKeyAttrs[0]].setConstraints( constraints );
1804
  }
1805

    
1806
  mValid = mPrimaryKeyType != PktUnknown;
1807

    
1808
  return mValid;
1809
}
1810

    
1811
/* static */
1812
QStringList QgsPostgresProvider::parseUriKey( const QString &key )
1813
{
1814
  if ( key.isEmpty() )
1815
    return QStringList();
1816

    
1817
  QStringList cols;
1818

    
1819
  // remove quotes from key list
1820
  if ( key.startsWith( '"' ) && key.endsWith( '"' ) )
1821
  {
1822
    int i = 1;
1823
    QString col;
1824
    while ( i < key.size() )
1825
    {
1826
      if ( key[i] == '"' )
1827
      {
1828
        if ( i + 1 < key.size() && key[i + 1] == '"' )
1829
        {
1830
          i++;
1831
        }
1832
        else
1833
        {
1834
          cols << col;
1835
          col.clear();
1836

    
1837
          if ( ++i == key.size() )
1838
            break;
1839

    
1840
          Q_ASSERT( key[i] == ',' );
1841
          i++;
1842
          Q_ASSERT( key[i] == '"' );
1843
          i++;
1844
          col.clear();
1845
          continue;
1846
        }
1847
      }
1848

    
1849
      col += key[i++];
1850
    }
1851
  }
1852
  else if ( key.contains( ',' ) )
1853
  {
1854
    cols = key.split( ',' );
1855
  }
1856
  else
1857
  {
1858
    cols << key;
1859
  }
1860

    
1861
  return cols;
1862
}
1863

    
1864
void QgsPostgresProvider::determinePrimaryKeyFromUriKeyColumn()
1865
{
1866
  QString primaryKey = mUri.keyColumn();
1867
  mPrimaryKeyType = PktUnknown;
1868

    
1869
  if ( !primaryKey.isEmpty() )
1870
  {
1871
    const QStringList cols = parseUriKey( primaryKey );
1872

    
1873
    primaryKey.clear();
1874
    QString del;
1875
    for ( const QString &col : cols )
1876
    {
1877
      primaryKey += del + quotedIdentifier( col );
1878
      del = QStringLiteral( "," );
1879
    }
1880

    
1881
    for ( const QString &col : cols )
1882
    {
1883
      int idx = fieldNameIndex( col );
1884
      if ( idx < 0 )
1885
      {
1886
        QgsMessageLog::logMessage( tr( "Key field '%1' for view/query not found." ).arg( col ), tr( "PostGIS" ) );
1887
        mPrimaryKeyAttrs.clear();
1888
        break;
1889
      }
1890

    
1891
      mPrimaryKeyAttrs << idx;
1892
    }
1893

    
1894
    if ( !mPrimaryKeyAttrs.isEmpty() )
1895
    {
1896
      bool unique = true;
1897
      if ( mCheckPrimaryKeyUnicity )
1898
      {
1899
        unique = uniqueData( primaryKey );
1900
      }
1901

    
1902
      if ( mUseEstimatedMetadata || unique )
1903
      {
1904
        mPrimaryKeyType = PktFidMap; // Map by default
1905
        if ( mPrimaryKeyAttrs.size() == 1 )
1906
        {
1907
          QgsField fld = mAttributeFields.at( mPrimaryKeyAttrs.at( 0 ) );
1908
          mPrimaryKeyType = pkType( fld );
1909
        }
1910
      }
1911
      else
1912
      {
1913
        QgsMessageLog::logMessage( tr( "Primary key field '%1' for view/query not unique." ).arg( primaryKey ), tr( "PostGIS" ) );
1914
      }
1915
    }
1916
    else
1917
    {
1918
      QgsMessageLog::logMessage( tr( "Keys for view/query undefined." ), tr( "PostGIS" ) );
1919
    }
1920
  }
1921
  else
1922
  {
1923
    QgsMessageLog::logMessage( tr( "No key field for view/query given." ), tr( "PostGIS" ) );
1924
  }
1925
}
1926

    
1927
bool QgsPostgresProvider::uniqueData( const QString &quotedColNames )
1928
{
1929
  // Check to see if the given columns contain unique data
1930
  QString sql = QStringLiteral( "SELECT count(distinct (%1))=count((%1)) FROM %2%3" )
1931
                  .arg( quotedColNames, mQuery, filterWhereClause() );
1932

    
1933
  QgsPostgresResult unique( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
1934

    
1935
  if ( unique.PQresultStatus() != PGRES_TUPLES_OK )
1936
  {
1937
    pushError( unique.PQresultErrorMessage() );
1938
    return false;
1939
  }
1940
  return unique.PQntuples() == 1 && unique.PQgetvalue( 0, 0 ).startsWith( 't' );
1941
}
1942

    
1943
// Returns the minimum value of an attribute
1944
QVariant QgsPostgresProvider::minimumValue( int index ) const
1945
{
1946
  try
1947
  {
1948
    // get the field name
1949
    QgsField fld = field( index );
1950
    QString sql = QStringLiteral( "SELECT min(%1) AS %1 FROM %2" )
1951
                    .arg( quotedIdentifier( fld.name() ), mQuery );
1952

    
1953
    if ( !mSqlWhereClause.isEmpty() )
1954
    {
1955
      sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
1956
    }
1957

    
1958
    sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql );
1959

    
1960
    QgsPostgresResult rmin( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
1961
    return convertValue( fld.type(), fld.subType(), rmin.PQgetvalue( 0, 0 ), fld.typeName() );
1962
  }
1963
  catch ( PGFieldNotFound )
1964
  {
1965
    return QVariant( QString() );
1966
  }
1967
}
1968

    
1969
// Returns the list of unique values of an attribute
1970
QSet<QVariant> QgsPostgresProvider::uniqueValues( int index, int limit ) const
1971
{
1972
  QSet<QVariant> uniqueValues;
1973

    
1974
  try
1975
  {
1976
    // get the field name
1977
    QgsField fld = field( index );
1978
    QString sql = QStringLiteral( "SELECT DISTINCT %1 FROM %2" )
1979
                    .arg( quotedIdentifier( fld.name() ), mQuery );
1980

    
1981
    if ( !mSqlWhereClause.isEmpty() )
1982
    {
1983
      sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
1984
    }
1985

    
1986
    sql += QStringLiteral( " ORDER BY %1" ).arg( quotedIdentifier( fld.name() ) );
1987

    
1988
    if ( limit >= 0 )
1989
    {
1990
      sql += QStringLiteral( " LIMIT %1" ).arg( limit );
1991
    }
1992

    
1993
    sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql );
1994

    
1995
    QgsPostgresResult res( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
1996
    if ( res.PQresultStatus() == PGRES_TUPLES_OK )
1997
    {
1998
      for ( int i = 0; i < res.PQntuples(); i++ )
1999
        uniqueValues.insert( convertValue( fld.type(), fld.subType(), res.PQgetvalue( i, 0 ), fld.typeName() ) );
2000
    }
2001
  }
2002
  catch ( PGFieldNotFound )
2003
  {
2004
  }
2005
  return uniqueValues;
2006
}
2007

    
2008
QStringList QgsPostgresProvider::uniqueStringsMatching( int index, const QString &substring, int limit, QgsFeedback *feedback ) const
2009
{
2010
  QStringList results;
2011

    
2012
  try
2013
  {
2014
    // get the field name
2015
    QgsField fld = field( index );
2016
    QString sql = QStringLiteral( "SELECT DISTINCT %1 FROM %2 WHERE" )
2017
                    .arg( quotedIdentifier( fld.name() ), mQuery );
2018

    
2019
    if ( !mSqlWhereClause.isEmpty() )
2020
    {
2021
      sql += QStringLiteral( " ( %1 ) AND " ).arg( mSqlWhereClause );
2022
    }
2023

    
2024
    sql += QStringLiteral( " %1::text ILIKE '%%2%'" ).arg( quotedIdentifier( fld.name() ), substring );
2025

    
2026

    
2027
    sql += QStringLiteral( " ORDER BY %1" ).arg( quotedIdentifier( fld.name() ) );
2028

    
2029
    if ( limit >= 0 )
2030
    {
2031
      sql += QStringLiteral( " LIMIT %1" ).arg( limit );
2032
    }
2033

    
2034
    sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql );
2035

    
2036
    QgsPostgresResult res( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), sql ) );
2037
    if ( res.PQresultStatus() == PGRES_TUPLES_OK )
2038
    {
2039
      for ( int i = 0; i < res.PQntuples(); i++ )
2040
      {
2041
        results << ( convertValue( fld.type(), fld.subType(), res.PQgetvalue( i, 0 ), fld.typeName() ) ).toString();
2042
        if ( feedback && feedback->isCanceled() )
2043
          break;
2044
      }
2045
    }
2046
  }
2047
  catch ( PGFieldNotFound )
2048
  {
2049
  }
2050
  return results;
2051
}
2052

    
2053
void QgsPostgresProvider::enumValues( int index, QStringList &enumList ) const
2054
{
2055
  if ( index < 0 || index >= mAttributeFields.count() )
2056
    return;
2057

    
2058
  if ( !mShared->fieldSupportsEnumValuesIsSet( index ) )
2059
  {
2060
    mShared->setFieldSupportsEnumValues( index, true );
2061
  }
2062
  else if ( !mShared->fieldSupportsEnumValues( index ) )
2063
  {
2064
    return;
2065
  }
2066

    
2067
  //find out type of index
2068
  const QString fieldName = mAttributeFields.at( index ).name();
2069
  QString typeName = mAttributeFields.at( index ).typeName();
2070

    
2071
  // Remove schema extension from typeName
2072
  typeName.remove( QRegularExpression( "^([^.]+\\.)+" ) );
2073

    
2074
  //is type an enum?
2075
  const QString typeSql = QStringLiteral( "SELECT typtype FROM pg_type WHERE typname=%1" ).arg( quotedValue( typeName ) );
2076
  QgsPostgresResult typeRes( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), typeSql ) );
2077
  if ( typeRes.PQresultStatus() != PGRES_TUPLES_OK || typeRes.PQntuples() < 1 )
2078
  {
2079
    mShared->setFieldSupportsEnumValues( index, false );
2080
    return;
2081
  }
2082

    
2083
  const QString typtype = typeRes.PQgetvalue( 0, 0 );
2084
  if ( typtype.compare( QLatin1String( "e" ), Qt::CaseInsensitive ) == 0 )
2085
  {
2086
    //try to read enum_range of attribute
2087
    if ( !parseEnumRange( enumList, fieldName ) )
2088
    {
2089
      mShared->setFieldSupportsEnumValues( index, false );
2090
    }
2091
  }
2092
  else
2093
  {
2094
    //is there a domain check constraint for the attribute?
2095
    if ( !parseDomainCheckConstraint( enumList, fieldName ) )
2096
    {
2097
      mShared->setFieldSupportsEnumValues( index, false );
2098
    }
2099
  }
2100
}
2101

    
2102
bool QgsPostgresProvider::parseEnumRange( QStringList &enumValues, const QString &attributeName ) const
2103
{
2104
  enumValues.clear();
2105

    
2106
  QString enumRangeSql = QStringLiteral( "SELECT enumlabel FROM pg_catalog.pg_enum WHERE enumtypid=(SELECT atttypid::regclass FROM pg_attribute WHERE attrelid=%1::regclass AND attname=%2)" )
2107
                           .arg( quotedValue( mQuery ), quotedValue( attributeName ) );
2108
  QgsPostgresResult enumRangeRes( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), enumRangeSql ) );
2109
  if ( enumRangeRes.PQresultStatus() != PGRES_TUPLES_OK )
2110
    return false;
2111

    
2112
  for ( int i = 0; i < enumRangeRes.PQntuples(); i++ )
2113
  {
2114
    enumValues << enumRangeRes.PQgetvalue( i, 0 );
2115
  }
2116

    
2117
  return true;
2118
}
2119

    
2120
bool QgsPostgresProvider::parseDomainCheckConstraint( QStringList &enumValues, const QString &attributeName ) const
2121
{
2122
  enumValues.clear();
2123

    
2124
  //is it a domain type with a check constraint?
2125
  QString domainSql = QStringLiteral( "SELECT domain_name, domain_schema FROM information_schema.columns WHERE table_name=%1 AND column_name=%2" ).arg( quotedValue( mTableName ), quotedValue( attributeName ) );
2126
  QgsPostgresResult domainResult( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), domainSql ) );
2127
  if ( domainResult.PQresultStatus() == PGRES_TUPLES_OK && domainResult.PQntuples() > 0 && !domainResult.PQgetvalue( 0, 0 ).isNull() )
2128
  {
2129
    QString domainCheckDefinitionSql;
2130
    if ( connectionRO()->pgVersion() < 120000 )
2131
    {
2132
      domainCheckDefinitionSql = QStringLiteral( ""
2133
                                                 "SELECT consrc FROM pg_constraint "
2134
                                                 "  WHERE contypid =("
2135
                                                 "    SELECT oid FROM pg_type "
2136
                                                 "      WHERE typname = %1 "
2137
                                                 "      AND typnamespace =("
2138
                                                 "        SELECT oid FROM pg_namespace WHERE nspname = %2"
2139
                                                 "      )"
2140
                                                 "    )" )
2141
                                   .arg( quotedValue( domainResult.PQgetvalue( 0, 0 ) ) )
2142
                                   .arg( quotedValue( domainResult.PQgetvalue( 0, 1 ) ) );
2143
    }
2144
    else
2145
    {
2146
      domainCheckDefinitionSql = QStringLiteral( ""
2147
                                                 "SELECT pg_catalog.pg_get_constraintdef( ( "
2148
                                                 "  SELECT oid FROM pg_constraint WHERE contypid = ( "
2149
                                                 "    SELECT oid FROM pg_type "
2150
                                                 "      WHERE typname = %1 "
2151
                                                 "      AND typnamespace =("
2152
                                                 "        SELECT oid FROM pg_namespace WHERE nspname = %2"
2153
                                                 "      )"
2154
                                                 "    )"
2155
                                                 "  ), true );" )
2156
                                   .arg( quotedValue( domainResult.PQgetvalue( 0, 0 ) ) )
2157
                                   .arg( quotedValue( domainResult.PQgetvalue( 0, 1 ) ) );
2158
    }
2159

    
2160
    QgsPostgresResult domainCheckRes( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), domainCheckDefinitionSql ) );
2161
    if ( domainCheckRes.PQresultStatus() == PGRES_TUPLES_OK && domainCheckRes.PQntuples() > 0 )
2162
    {
2163
      QString checkDefinition = domainCheckRes.PQgetvalue( 0, 0 );
2164

    
2165
      //we assume that the constraint is of the following form:
2166
      //(VALUE = ANY (ARRAY['a'::text, 'b'::text, 'c'::text, 'd'::text]))
2167
      //normally, PostgreSQL creates that if the constraint has been specified as 'VALUE in ('a', 'b', 'c', 'd')
2168
      const thread_local QRegularExpression definitionRegExp( "VALUE\\s*=\\s*ANY\\s*\\(\\s*ARRAY\\s*\\[" );
2169
      int anyPos = checkDefinition.indexOf( definitionRegExp );
2170
      int arrayPosition = checkDefinition.lastIndexOf( QLatin1String( "ARRAY[" ) );
2171
      int closingBracketPos = checkDefinition.indexOf( ']', arrayPosition + 6 );
2172

    
2173
      if ( anyPos == -1 || anyPos >= arrayPosition )
2174
      {
2175
        return false; //constraint has not the required format
2176
      }
2177

    
2178
      if ( arrayPosition != -1 )
2179
      {
2180
        QString valueList = checkDefinition.mid( arrayPosition + 6, closingBracketPos );
2181
        const QStringList commaSeparation = valueList.split( ',', Qt::SkipEmptyParts );
2182
        QStringList::const_iterator cIt = commaSeparation.constBegin();
2183
        for ( ; cIt != commaSeparation.constEnd(); ++cIt )
2184
        {
2185
          //get string between ''
2186
          int beginQuotePos = cIt->indexOf( '\'' );
2187
          int endQuotePos = cIt->lastIndexOf( '\'' );
2188
          if ( beginQuotePos != -1 && ( endQuotePos - beginQuotePos ) > 1 )
2189
          {
2190
            enumValues << cIt->mid( beginQuotePos + 1, endQuotePos - beginQuotePos - 1 );
2191
          }
2192
        }
2193
      }
2194
      return true;
2195
    }
2196
  }
2197
  return false;
2198
}
2199

    
2200
// Returns the maximum value of an attribute
2201
QVariant QgsPostgresProvider::maximumValue( int index ) const
2202
{
2203
  try
2204
  {
2205
    // get the field name
2206
    QgsField fld = field( index );
2207
    QString sql = QStringLiteral( "SELECT max(%1) AS %1 FROM %2" )
2208
                    .arg( quotedIdentifier( fld.name() ), mQuery );
2209

    
2210
    if ( !mSqlWhereClause.isEmpty() )
2211
    {
2212
      sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
2213
    }
2214

    
2215
    sql = QStringLiteral( "SELECT %1 FROM (%2) foo" ).arg( connectionRO()->fieldExpression( fld ), sql );
2216

    
2217
    QgsPostgresResult rmax( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
2218

    
2219
    return convertValue( fld.type(), fld.subType(), rmax.PQgetvalue( 0, 0 ), fld.typeName() );
2220
  }
2221
  catch ( PGFieldNotFound )
2222
  {
2223
    return QVariant( QString() );
2224
  }
2225
}
2226

    
2227

    
2228
bool QgsPostgresProvider::isValid() const
2229
{
2230
  return mValid;
2231
}
2232

    
2233
Qgis::ProviderStyleStorageCapabilities QgsPostgresProvider::styleStorageCapabilities() const
2234
{
2235
  Qgis::ProviderStyleStorageCapabilities storageCapabilities;
2236
  if ( isValid() )
2237
  {
2238
    storageCapabilities |= Qgis::ProviderStyleStorageCapability::SaveToDatabase;
2239
    storageCapabilities |= Qgis::ProviderStyleStorageCapability::LoadFromDatabase;
2240
    storageCapabilities |= Qgis::ProviderStyleStorageCapability::DeleteFromDatabase;
2241
  }
2242
  return storageCapabilities;
2243
}
2244

    
2245
QString QgsPostgresProvider::defaultValueClause( int fieldId ) const
2246
{
2247
  QString defVal = mDefaultValues.value( fieldId, QString() );
2248

    
2249
  // with generated columns (PostgreSQL 12+), the provider will ALWAYS evaluate the default values.
2250
  // The only acceptable value for such columns on INSERT or UPDATE clauses is the keyword "DEFAULT".
2251
  // Here, we return the expression used to generate the field value, so the
2252
  // user can see what is happening when inserting a new feature.
2253
  // On inserting a new feature or updating a generated field, this is
2254
  // omitted from the generated queries.
2255
  // See https://www.postgresql.org/docs/12/ddl-generated-columns.html
2256
  if ( mGeneratedValues.contains( fieldId ) )
2257
  {
2258
    return defVal;
2259
  }
2260

    
2261
  if ( !providerProperty( EvaluateDefaultValues, false ).toBool() && !defVal.isEmpty() )
2262
  {
2263
    return defVal;
2264
  }
2265

    
2266
  return QString();
2267
}
2268

    
2269
QVariant QgsPostgresProvider::defaultValue( int fieldId ) const
2270
{
2271
  QString defVal = mDefaultValues.value( fieldId, QString() );
2272

    
2273
  if ( providerProperty( EvaluateDefaultValues, false ).toBool() && !defVal.isEmpty() )
2274
  {
2275
    QgsField fld = field( fieldId );
2276

    
2277
    QgsPostgresResult res( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), QStringLiteral( "SELECT %1" ).arg( defVal ) ) );
2278

    
2279
    if ( res.result() )
2280
    {
2281
      return convertValue( fld.type(), fld.subType(), res.PQgetvalue( 0, 0 ), fld.typeName() );
2282
    }
2283
    else
2284
    {
2285
      pushError( tr( "Could not execute query" ) );
2286
      return QVariant();
2287
    }
2288
  }
2289

    
2290
  return QVariant();
2291
}
2292

    
2293
bool QgsPostgresProvider::skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint, const QVariant &value ) const
2294
{
2295
  if ( providerProperty( EvaluateDefaultValues, false ).toBool() )
2296
  {
2297
    return !mDefaultValues.value( fieldIndex ).isEmpty();
2298
  }
2299
  else
2300
  {
2301
    // stricter check - if we are evaluating default values only on commit then we can only bypass the check
2302
    // if the attribute values matches the original default clause
2303
    return mDefaultValues.contains( fieldIndex ) && mDefaultValues.value( fieldIndex ) == value.toString() && !QgsVariantUtils::isNull( value );
2304
  }
2305
}
2306

    
2307
QString QgsPostgresProvider::paramValue( const QString &fieldValue, const QString &defaultValue ) const
2308
{
2309
  if ( fieldValue.isNull() )
2310
    return QString();
2311

    
2312
  if ( fieldValue == defaultValue && !defaultValue.isNull() )
2313
  {
2314
    QgsPostgresResult result( connectionRO()->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), QStringLiteral( "SELECT %1" ).arg( defaultValue ) ) );
2315
    if ( result.PQresultStatus() != PGRES_TUPLES_OK )
2316
      throw PGException( result );
2317

    
2318
    return result.PQgetvalue( 0, 0 );
2319
  }
2320

    
2321
  return fieldValue;
2322
}
2323

    
2324

    
2325
/* private */
2326
bool QgsPostgresProvider::getTopoLayerInfo()
2327
{
2328
  QString sql = QStringLiteral( R"SQL(
2329
    SELECT
2330
      t.name,
2331
      l.layer_id,
2332
      l.level,
2333
      l.feature_type
2334
    FROM topology.layer l
2335
    JOIN topology.topology t ON (
2336
      l.topology_id = t.id
2337
    )
2338
    WHERE l.schema_name=%1
2339
    AND l.table_name=%2 AND l.feature_column=%3
2340
  )SQL" )
2341
                  .arg( quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( mGeometryColumn ) );
2342
  QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
2343
  if ( result.PQresultStatus() != PGRES_TUPLES_OK )
2344
  {
2345
    throw PGException( result ); // we should probably not do this
2346
  }
2347
  if ( result.PQntuples() < 1 )
2348
  {
2349
    QgsMessageLog::logMessage( tr( "Could not find topology of layer %1.%2.%3" ).arg( quotedValue( mSchemaName ), quotedValue( mTableName ), quotedValue( mGeometryColumn ) ), tr( "PostGIS" ) );
2350
    return false;
2351
  }
2352
  mTopoLayerInfo.topologyName = result.PQgetvalue( 0, 0 );
2353
  mTopoLayerInfo.layerId = result.PQgetvalue( 0, 1 ).toLong();
2354
  mTopoLayerInfo.layerLevel = result.PQgetvalue( 0, 2 ).toInt();
2355
  switch ( result.PQgetvalue( 0, 3 ).toInt() )
2356
  {
2357
    case 1:
2358
      mTopoLayerInfo.featureType = TopoLayerInfo::Puntal;
2359
      break;
2360
    case 2:
2361
      mTopoLayerInfo.featureType = TopoLayerInfo::Lineal;
2362
      break;
2363
    case 3:
2364
      mTopoLayerInfo.featureType = TopoLayerInfo::Polygonal;
2365
      break;
2366
    case 4:
2367
    default:
2368
      mTopoLayerInfo.featureType = TopoLayerInfo::Mixed;
2369
      break;
2370
  }
2371
  return true;
2372
}
2373

    
2374
/* private */
2375
void QgsPostgresProvider::dropOrphanedTopoGeoms()
2376
{
2377
  QString sql = QString( "DELETE FROM %1.relation WHERE layer_id = %2 AND "
2378
                         "topogeo_id NOT IN ( SELECT id(%3) FROM %4.%5 )" )
2379
                  .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) )
2380
                  .arg( mTopoLayerInfo.layerId )
2381
                  .arg( quotedIdentifier( mGeometryColumn ), quotedIdentifier( mSchemaName ), quotedIdentifier( mTableName ) );
2382

    
2383
  QgsDebugMsgLevel( "TopoGeom orphans cleanup query: " + sql, 2 );
2384

    
2385
  connectionRW()->LoggedPQexecNR( "QgsPostgresProvider", sql );
2386
}
2387

    
2388
QString QgsPostgresProvider::geomParam( int offset ) const
2389
{
2390
  QString geometry;
2391

    
2392
  bool forceMulti = false;
2393

    
2394
  if ( mSpatialColType != SctTopoGeometry )
2395
  {
2396
    forceMulti = QgsWkbTypes::isMultiType( wkbType() );
2397
  }
2398

    
2399
  if ( mSpatialColType == SctTopoGeometry )
2400
  {
2401
    geometry += QLatin1String( "toTopoGeom(" );
2402
  }
2403

    
2404
  if ( forceMulti )
2405
  {
2406
    geometry += connectionRO()->majorVersion() < 2 ? "multi(" : "st_multi(";
2407
  }
2408

    
2409
  geometry += QStringLiteral( "%1($%2%3,%4)" )
2410
                .arg( connectionRO()->majorVersion() < 2 ? "geomfromwkb" : "st_geomfromwkb" )
2411
                .arg( offset )
2412
                .arg( connectionRO()->useWkbHex() ? "" : "::bytea", mRequestedSrid.isEmpty() ? mDetectedSrid : mRequestedSrid );
2413

    
2414
  if ( forceMulti )
2415
  {
2416
    geometry += ')';
2417
  }
2418

    
2419
  if ( mSpatialColType == SctTopoGeometry )
2420
  {
2421
    geometry += QStringLiteral( ",%1,%2)" )
2422
                  .arg( quotedValue( mTopoLayerInfo.topologyName ) )
2423
                  .arg( mTopoLayerInfo.layerId );
2424
  }
2425

    
2426
  return geometry;
2427
}
2428

    
2429
bool QgsPostgresProvider::addFeatures( QgsFeatureList &flist, Flags flags )
2430
{
2431
  if ( flist.isEmpty() )
2432
    return true;
2433

    
2434
  if ( mIsQuery )
2435
    return false;
2436

    
2437
  QgsPostgresConn *conn = connectionRW();
2438
  if ( !conn )
2439
  {
2440
    return false;
2441
  }
2442
  conn->lock();
2443

    
2444
  bool returnvalue = true;
2445

    
2446
  try
2447
  {
2448
    conn->begin();
2449

    
2450
    // Prepare the INSERT statement
2451
    QString insert = QStringLiteral( "INSERT INTO %1(" ).arg( mQuery );
2452
    QString values;
2453
    QString delim;
2454
    int offset = 1;
2455

    
2456
    QStringList defaultValues;
2457
    QList<int> fieldId;
2458

    
2459
    if ( !mGeometryColumn.isNull() )
2460
    {
2461
      insert += quotedIdentifier( mGeometryColumn );
2462

    
2463
      values += geomParam( offset++ );
2464

    
2465
      delim = ',';
2466
    }
2467

    
2468
    // Optimization: if we have a single primary key column whose default value
2469
    // is a sequence, and that none of the features have a value set for that
2470
    // column, then we can completely omit inserting it.
2471
    bool skipSinglePKField = false;
2472
    bool overrideIdentity = false;
2473

    
2474
    if ( ( mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktUint64 ) )
2475
    {
2476
      if ( mPrimaryKeyAttrs.size() == 1 && defaultValueClause( mPrimaryKeyAttrs[0] ).startsWith( "nextval(" ) )
2477
      {
2478
        bool foundNonEmptyPK = false;
2479
        int idx = mPrimaryKeyAttrs[0];
2480
        QString defaultValue = defaultValueClause( idx );
2481
        for ( int i = 0; i < flist.size(); i++ )
2482
        {
2483
          QgsAttributes attrs2 = flist[i].attributes();
2484
          QVariant v2 = attrs2.value( idx, QgsVariantUtils::createNullVariant( QMetaType::Type::Int ) );
2485
          // a PK field with a sequence val is auto populate by QGIS with this default
2486
          // we are only interested in non default values
2487
          if ( !QgsVariantUtils::isNull( v2 ) && v2.toString() != defaultValue )
2488
          {
2489
            foundNonEmptyPK = true;
2490
            break;
2491
          }
2492
        }
2493
        skipSinglePKField = !foundNonEmptyPK;
2494
      }
2495

    
2496
      if ( !skipSinglePKField )
2497
      {
2498
        for ( int idx : mPrimaryKeyAttrs )
2499
        {
2500
          if ( mIdentityFields[idx] == 'a' )
2501
            overrideIdentity = true;
2502
          insert += delim + quotedIdentifier( field( idx ).name() );
2503
          values += delim + QStringLiteral( "$%1" ).arg( defaultValues.size() + offset );
2504
          delim = ',';
2505
          fieldId << idx;
2506
          defaultValues << defaultValueClause( idx );
2507
        }
2508
      }
2509
    }
2510

    
2511
    QgsAttributes attributevec = flist[0].attributes();
2512

    
2513
    // look for unique attribute values to place in statement instead of passing as parameter
2514
    // e.g. for defaults
2515
    for ( int idx = 0; idx < attributevec.count(); ++idx )
2516
    {
2517
      QVariant v = attributevec.value( idx, QgsVariantUtils::createNullVariant( QMetaType::Type::Int ) ); // default to NULL for missing attributes
2518
      if ( skipSinglePKField && idx == mPrimaryKeyAttrs[0] )
2519
        continue;
2520
      if ( fieldId.contains( idx ) )
2521
        continue;
2522

    
2523
      if ( idx >= mAttributeFields.count() )
2524
        continue;
2525

    
2526
      QString fieldname = mAttributeFields.at( idx ).name();
2527

    
2528
      if ( !mGeneratedValues.value( idx, QString() ).isEmpty() )
2529
      {
2530
        QgsDebugMsgLevel( QStringLiteral( "Skipping field %1 (idx %2) which is GENERATED." ).arg( fieldname, QString::number( idx ) ), 2 );
2531
        continue;
2532
      }
2533

    
2534
      QString fieldTypeName = mAttributeFields.at( idx ).typeName();
2535

    
2536
      QgsDebugMsgLevel( "Checking field against: " + fieldname, 2 );
2537

    
2538
      if ( fieldname.isEmpty() || fieldname == mGeometryColumn )
2539
        continue;
2540

    
2541
      int i;
2542
      for ( i = 1; i < flist.size(); i++ )
2543
      {
2544
        QgsAttributes attrs2 = flist[i].attributes();
2545
        QVariant v2 = attrs2.value( idx, QgsVariantUtils::createNullVariant( QMetaType::Type::Int ) ); // default to NULL for missing attributes
2546

    
2547
        if ( v2 != v )
2548
          break;
2549
      }
2550

    
2551
      insert += delim + quotedIdentifier( fieldname );
2552

    
2553
      if ( mIdentityFields[idx] == 'a' )
2554
        overrideIdentity = true;
2555

    
2556
      QString defVal = defaultValueClause( idx );
2557

    
2558
      if ( i == flist.size() )
2559
      {
2560
        if ( qgsVariantEqual( v, defVal ) )
2561
        {
2562
          if ( defVal.isNull() )
2563
          {
2564
            values += delim + "NULL";
2565
          }
2566
          else
2567
          {
2568
            values += delim + defVal;
2569
          }
2570
        }
2571
        else if ( fieldTypeName == QLatin1String( "geometry" ) )
2572
        {
2573
          QString val = geomAttrToString( v, connectionRO() );
2574
          values += QStringLiteral( "%1%2(%3)" )
2575
                      .arg( delim, connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( val ) );
2576
        }
2577
        else if ( fieldTypeName == QLatin1String( "geography" ) )
2578
        {
2579
          values += QStringLiteral( "%1st_geographyfromtext(%2)" )
2580
                      .arg( delim, quotedValue( v.toString() ) );
2581
        }
2582
        else if ( fieldTypeName == QLatin1String( "jsonb" ) )
2583
        {
2584
          values += delim + quotedJsonValue( v ) + QStringLiteral( "::jsonb" );
2585
        }
2586
        else if ( fieldTypeName == QLatin1String( "json" ) )
2587
        {
2588
          values += delim + quotedJsonValue( v ) + QStringLiteral( "::json" );
2589
        }
2590
        else if ( fieldTypeName == QLatin1String( "bytea" ) )
2591
        {
2592
          values += delim + quotedByteaValue( v );
2593
        }
2594
        //TODO: convert arrays and hstore to native types
2595
        else
2596
        {
2597
          values += delim + quotedValue( v );
2598
        }
2599
      }
2600
      else
2601
      {
2602
        // value is not unique => add parameter
2603
        if ( fieldTypeName == QLatin1String( "geometry" ) )
2604
        {
2605
          values += QStringLiteral( "%1%2($%3)" )
2606
                      .arg( delim, connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt" )
2607
                      .arg( defaultValues.size() + offset );
2608
        }
2609
        else if ( fieldTypeName == QLatin1String( "geography" ) )
2610
        {
2611
          values += QStringLiteral( "%1st_geographyfromtext($%2)" )
2612
                      .arg( delim )
2613
                      .arg( defaultValues.size() + offset );
2614
        }
2615
        else
2616
        {
2617
          values += QStringLiteral( "%1$%2" )
2618
                      .arg( delim )
2619
                      .arg( defaultValues.size() + offset );
2620
        }
2621
        defaultValues.append( defVal );
2622
        fieldId.append( idx );
2623
      }
2624

    
2625
      delim = ',';
2626
    }
2627

    
2628
    insert += QStringLiteral( ") %1VALUES (%2)" ).arg( overrideIdentity ? "OVERRIDING SYSTEM VALUE " : "" ).arg( values );
2629

    
2630
    if ( !( flags & QgsFeatureSink::FastInsert ) )
2631
    {
2632
      if ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktUint64 )
2633
      {
2634
        insert += QLatin1String( " RETURNING " );
2635

    
2636
        QString delim;
2637
        const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
2638
        for ( int idx : constMPrimaryKeyAttrs )
2639
        {
2640
          insert += delim + quotedIdentifier( mAttributeFields.at( idx ).name() );
2641
          delim = ',';
2642
        }
2643
      }
2644
    }
2645

    
2646
    QgsDebugMsgLevel( QStringLiteral( "prepare addfeatures: %1" ).arg( insert ), 2 );
2647
    QgsPostgresResult stmt( conn->PQprepare( QStringLiteral( "addfeatures" ), insert, fieldId.size() + offset - 1, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN ) );
2648

    
2649
    if ( stmt.PQresultStatus() != PGRES_COMMAND_OK )
2650
      throw PGException( stmt );
2651

    
2652
    for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); ++features )
2653
    {
2654
      QgsAttributes attrs = features->attributes();
2655

    
2656
      QStringList params;
2657
      if ( !mGeometryColumn.isNull() )
2658
      {
2659
        appendGeomParam( features->geometry(), params );
2660
      }
2661

    
2662
      params.reserve( fieldId.size() );
2663
      for ( int i = 0; i < fieldId.size(); i++ )
2664
      {
2665
        int attrIdx = fieldId[i];
2666
        QVariant value = attrIdx < attrs.length() ? attrs.at( attrIdx ) : QgsVariantUtils::createNullVariant( QMetaType::Type::Int );
2667

    
2668
        QString v;
2669
        if ( QgsVariantUtils::isNull( value ) )
2670
        {
2671
          QgsField fld = field( attrIdx );
2672
          v = paramValue( defaultValues[i], defaultValues[i] );
2673
          features->setAttribute( attrIdx, convertValue( fld.type(), fld.subType(), v, fld.typeName() ) );
2674
        }
2675
        else
2676
        {
2677
          // the conversion functions expects the list as a string, so convert it
2678
          if ( value.userType() == QMetaType::Type::QStringList )
2679
          {
2680
            QStringList list_vals = value.toStringList();
2681
            // all strings need to be double quoted to allow special postgres
2682
            // array characters such as {, or whitespace in the string
2683
            // but we need to escape all double quotes and backslashes
2684
            list_vals.replaceInStrings( "\\", "\\\\" );
2685
            list_vals.replaceInStrings( "\"", "\\\"" );
2686
            v = QStringLiteral( "{\"" ) + value.toStringList().join( QLatin1String( "\",\"" ) ) + QStringLiteral( "\"}" );
2687
          }
2688
          else if ( value.userType() == QMetaType::Type::QVariantList )
2689
          {
2690
            v = "{" + value.toStringList().join( "," ) + "}";
2691
          }
2692
          else
2693
          {
2694
            v = paramValue( value.toString(), defaultValues[i] );
2695
          }
2696

    
2697
          if ( v != value.toString() )
2698
          {
2699
            QgsField fld = field( attrIdx );
2700
            features->setAttribute( attrIdx, convertValue( fld.type(), fld.subType(), v, fld.typeName() ) );
2701
          }
2702
        }
2703

    
2704
        params << v;
2705
      }
2706

    
2707
      QgsPostgresResult result( conn->PQexecPrepared( QStringLiteral( "addfeatures" ), params, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN ) );
2708

    
2709
      if ( !( flags & QgsFeatureSink::FastInsert ) && result.PQresultStatus() == PGRES_TUPLES_OK )
2710
      {
2711
        for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
2712
        {
2713
          const int idx = mPrimaryKeyAttrs.at( i );
2714
          const QgsField fld = mAttributeFields.at( idx );
2715
          features->setAttribute( idx, convertValue( fld.type(), fld.subType(), result.PQgetvalue( 0, i ), fld.typeName() ) );
2716
        }
2717
      }
2718
      else if ( result.PQresultStatus() != PGRES_COMMAND_OK )
2719
        throw PGException( result );
2720

    
2721
      if ( !( flags & QgsFeatureSink::FastInsert ) && mPrimaryKeyType == PktOid )
2722
      {
2723
        features->setId( result.PQoidValue() );
2724
        QgsDebugMsgLevel( QStringLiteral( "new fid=%1" ).arg( features->id() ), 4 );
2725
      }
2726
    }
2727

    
2728
    if ( !( flags & QgsFeatureSink::FastInsert ) )
2729
    {
2730
      // update feature ids
2731
      if ( mPrimaryKeyType == PktInt || mPrimaryKeyType == PktInt64 || mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktUint64 )
2732
      {
2733
        for ( QgsFeatureList::iterator features = flist.begin(); features != flist.end(); ++features )
2734
        {
2735
          QgsAttributes attrs = features->attributes();
2736

    
2737
          if ( mPrimaryKeyType == PktInt )
2738
          {
2739
            features->setId( PKINT2FID( STRING_TO_FID( attrs.at( mPrimaryKeyAttrs.at( 0 ) ) ) ) );
2740
          }
2741
          else
2742
          {
2743
            QVariantList primaryKeyVals;
2744

    
2745
            const auto constMPrimaryKeyAttrs = mPrimaryKeyAttrs;
2746
            for ( int idx : constMPrimaryKeyAttrs )
2747
            {
2748
              primaryKeyVals << attrs.at( idx );
2749
            }
2750

    
2751
            features->setId( mShared->lookupFid( primaryKeyVals ) );
2752
          }
2753
          QgsDebugMsgLevel( QStringLiteral( "new fid=%1" ).arg( features->id() ), 4 );
2754
        }
2755
      }
2756
    }
2757

    
2758
    conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE addfeatures" ) );
2759

    
2760
    returnvalue &= conn->commit();
2761
    if ( mTransaction )
2762
      mTransaction->dirtyLastSavePoint();
2763

    
2764
    mShared->addFeaturesCounted( flist.size() );
2765
  }
2766
  catch ( PGException &e )
2767
  {
2768
    pushError( tr( "PostGIS error while adding features: %1" ).arg( e.errorMessage() ) );
2769
    conn->rollback();
2770
    conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE addfeatures" ) );
2771
    returnvalue = false;
2772
  }
2773

    
2774
  conn->unlock();
2775
  return returnvalue;
2776
}
2777

    
2778
bool QgsPostgresProvider::deleteFeatures( const QgsFeatureIds &ids )
2779
{
2780
  if ( ids.isEmpty() )
2781
    return true;
2782

    
2783
  bool returnvalue = true;
2784

    
2785
  if ( mIsQuery )
2786
  {
2787
    QgsDebugError( QStringLiteral( "Cannot delete features (is a query)" ) );
2788
    return false;
2789
  }
2790

    
2791
  QgsPostgresConn *conn = connectionRW();
2792
  if ( !conn )
2793
  {
2794
    return false;
2795
  }
2796
  conn->lock();
2797

    
2798
  try
2799
  {
2800
    conn->begin();
2801

    
2802
    QgsFeatureIds chunkIds;
2803
    const int countIds = ids.size();
2804
    int i = 0;
2805
    for ( QgsFeatureIds::const_iterator it = ids.constBegin(); it != ids.constEnd(); ++it )
2806
    {
2807
      // create chunks of fids to delete, the last chunk may be smaller
2808
      chunkIds.insert( *it );
2809
      i++;
2810
      if ( chunkIds.size() < 5000 && i < countIds )
2811
        continue;
2812

    
2813
      const QString sql = QStringLiteral( "DELETE FROM %1 WHERE %2" )
2814
                            .arg( mQuery, whereClause( chunkIds ) );
2815
      QgsDebugMsgLevel( "delete sql: " + sql, 2 );
2816

    
2817
      //send DELETE statement and do error handling
2818
      QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
2819
      if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
2820
        throw PGException( result );
2821

    
2822
      for ( QgsFeatureIds::const_iterator chunkIt = chunkIds.constBegin(); chunkIt != chunkIds.constEnd(); ++chunkIt )
2823
      {
2824
        mShared->removeFid( *chunkIt );
2825
      }
2826
      chunkIds.clear();
2827
    }
2828

    
2829
    returnvalue &= conn->commit();
2830
    if ( mTransaction )
2831
      mTransaction->dirtyLastSavePoint();
2832

    
2833
    if ( mSpatialColType == SctTopoGeometry )
2834
    {
2835
      // NOTE: in presence of multiple TopoGeometry objects
2836
      //       for the same table or when deleting a Geometry
2837
      //       layer _also_ having a TopoGeometry component,
2838
      //       orphans would still be left.
2839
      // TODO: decouple layer from table and signal table when
2840
      //       records are added or removed
2841
      dropOrphanedTopoGeoms();
2842
    }
2843

    
2844
    mShared->addFeaturesCounted( -ids.size() );
2845
  }
2846
  catch ( PGException &e )
2847
  {
2848
    pushError( tr( "PostGIS error while deleting features: %1" ).arg( e.errorMessage() ) );
2849
    conn->rollback();
2850
    returnvalue = false;
2851
  }
2852

    
2853
  conn->unlock();
2854
  return returnvalue;
2855
}
2856

    
2857
bool QgsPostgresProvider::truncate()
2858
{
2859
  bool returnvalue = true;
2860

    
2861
  if ( mIsQuery )
2862
  {
2863
    QgsDebugError( QStringLiteral( "Cannot truncate (is a query)" ) );
2864
    return false;
2865
  }
2866

    
2867
  QgsPostgresConn *conn = connectionRW();
2868
  if ( !conn )
2869
  {
2870
    return false;
2871
  }
2872
  conn->lock();
2873

    
2874
  try
2875
  {
2876
    conn->begin();
2877

    
2878
    QString sql = QStringLiteral( "TRUNCATE %1" ).arg( mQuery );
2879
    QgsDebugMsgLevel( "truncate sql: " + sql, 2 );
2880

    
2881
    //send truncate statement and do error handling
2882
    QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
2883
    if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
2884
      throw PGException( result );
2885

    
2886
    returnvalue &= conn->commit();
2887
    if ( mTransaction )
2888
      mTransaction->dirtyLastSavePoint();
2889

    
2890
    if ( returnvalue )
2891
    {
2892
      if ( mSpatialColType == SctTopoGeometry )
2893
      {
2894
        // NOTE: in presence of multiple TopoGeometry objects
2895
        //       for the same table or when deleting a Geometry
2896
        //       layer _also_ having a TopoGeometry component,
2897
        //       orphans would still be left.
2898
        // TODO: decouple layer from table and signal table when
2899
        //       records are added or removed
2900
        dropOrphanedTopoGeoms();
2901
      }
2902
      mShared->clear();
2903
    }
2904
  }
2905
  catch ( PGException &e )
2906
  {
2907
    pushError( tr( "PostGIS error while truncating: %1" ).arg( e.errorMessage() ) );
2908
    conn->rollback();
2909
    returnvalue = false;
2910
  }
2911

    
2912
  conn->unlock();
2913
  return returnvalue;
2914
}
2915

    
2916
bool QgsPostgresProvider::addAttributes( const QList<QgsField> &attributes )
2917
{
2918
  bool returnvalue = true;
2919

    
2920
  if ( mIsQuery )
2921
    return false;
2922

    
2923
  if ( attributes.isEmpty() )
2924
    return true;
2925

    
2926
  QgsPostgresConn *conn = connectionRW();
2927
  if ( !conn )
2928
  {
2929
    return false;
2930
  }
2931
  conn->lock();
2932

    
2933
  try
2934
  {
2935
    conn->begin();
2936

    
2937
    QString delim;
2938
    QString sql = QStringLiteral( "ALTER TABLE %1 " ).arg( mQuery );
2939
    for ( QList<QgsField>::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter )
2940
    {
2941
      QString type = iter->typeName();
2942
      if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) )
2943
      {
2944
        if ( iter->length() > 0 )
2945
          type = QStringLiteral( "%1(%2)" ).arg( type ).arg( iter->length() );
2946
      }
2947
      else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) )
2948
      {
2949
        if ( iter->length() > 0 && iter->precision() > 0 )
2950
          type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( iter->length() ).arg( iter->precision() );
2951
      }
2952
      sql.append( QStringLiteral( "%1ADD COLUMN %2 %3" ).arg( delim, quotedIdentifier( iter->name() ), type ) );
2953
      delim = ',';
2954
    }
2955

    
2956
    //send sql statement and do error handling
2957
    QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
2958
    if ( result.PQresultStatus() != PGRES_COMMAND_OK )
2959
      throw PGException( result );
2960

    
2961
    for ( QList<QgsField>::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter )
2962
    {
2963
      if ( !iter->comment().isEmpty() )
2964
      {
2965
        sql = QStringLiteral( "COMMENT ON COLUMN %1.%2 IS %3" )
2966
                .arg( mQuery, quotedIdentifier( iter->name() ), quotedValue( iter->comment() ) );
2967
        result = conn->LoggedPQexec( "QgsPostgresProvider", sql );
2968
        if ( result.PQresultStatus() != PGRES_COMMAND_OK )
2969
          throw PGException( result );
2970
      }
2971
    }
2972

    
2973
    returnvalue &= conn->commit();
2974
    if ( mTransaction )
2975
      mTransaction->dirtyLastSavePoint();
2976
  }
2977
  catch ( PGException &e )
2978
  {
2979
    pushError( tr( "PostGIS error while adding attributes: %1" ).arg( e.errorMessage() ) );
2980
    conn->rollback();
2981
    returnvalue = false;
2982
  }
2983

    
2984
  loadFields();
2985
  conn->unlock();
2986
  return returnvalue;
2987
}
2988

    
2989
bool QgsPostgresProvider::deleteAttributes( const QgsAttributeIds &ids )
2990
{
2991
  bool returnvalue = true;
2992

    
2993
  if ( mIsQuery )
2994
    return false;
2995

    
2996
  QgsPostgresConn *conn = connectionRW();
2997
  if ( !conn )
2998
  {
2999
    return false;
3000
  }
3001
  conn->lock();
3002

    
3003
  try
3004
  {
3005
    conn->begin();
3006

    
3007
    QList<int> idsList = ids.values();
3008
    std::sort( idsList.begin(), idsList.end(), std::greater<int>() );
3009

    
3010
    for ( auto iter = idsList.constBegin(); iter != idsList.constEnd(); ++iter )
3011
    {
3012
      int index = *iter;
3013
      if ( index < 0 || index >= mAttributeFields.count() )
3014
        continue;
3015

    
3016
      QString column = mAttributeFields.at( index ).name();
3017
      QString sql = QStringLiteral( "ALTER TABLE %1 DROP COLUMN %2" )
3018
                      .arg( mQuery, quotedIdentifier( column ) );
3019

    
3020
      //send sql statement and do error handling
3021
      QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
3022
      if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3023
        throw PGException( result );
3024

    
3025
      //delete the attribute from mAttributeFields
3026
      mAttributeFields.remove( index );
3027
    }
3028

    
3029
    returnvalue &= conn->commit();
3030
    if ( mTransaction )
3031
      mTransaction->dirtyLastSavePoint();
3032
  }
3033
  catch ( PGException &e )
3034
  {
3035
    pushError( tr( "PostGIS error while deleting attributes: %1" ).arg( e.errorMessage() ) );
3036
    conn->rollback();
3037
    returnvalue = false;
3038
  }
3039

    
3040
  loadFields();
3041
  conn->unlock();
3042
  return returnvalue;
3043
}
3044

    
3045
bool QgsPostgresProvider::renameAttributes( const QgsFieldNameMap &renamedAttributes )
3046
{
3047
  if ( mIsQuery )
3048
    return false;
3049

    
3050

    
3051
  QString sql = QStringLiteral( "BEGIN;" );
3052

    
3053
  QgsFieldNameMap::const_iterator renameIt = renamedAttributes.constBegin();
3054
  bool returnvalue = true;
3055
  for ( ; renameIt != renamedAttributes.constEnd(); ++renameIt )
3056
  {
3057
    int fieldIndex = renameIt.key();
3058
    if ( fieldIndex < 0 || fieldIndex >= mAttributeFields.count() )
3059
    {
3060
      pushError( tr( "Invalid attribute index: %1" ).arg( fieldIndex ) );
3061
      return false;
3062
    }
3063
    if ( mAttributeFields.indexFromName( renameIt.value() ) >= 0 )
3064
    {
3065
      //field name already in use
3066
      pushError( tr( "Error renaming field %1: name '%2' already exists" ).arg( fieldIndex ).arg( renameIt.value() ) );
3067
      return false;
3068
    }
3069

    
3070
    sql += QStringLiteral( "ALTER TABLE %1 RENAME COLUMN %2 TO %3;" )
3071
             .arg( mQuery, quotedIdentifier( mAttributeFields.at( fieldIndex ).name() ), quotedIdentifier( renameIt.value() ) );
3072
  }
3073
  sql += QLatin1String( "COMMIT;" );
3074

    
3075
  QgsPostgresConn *conn = connectionRW();
3076
  if ( !conn )
3077
  {
3078
    return false;
3079
  }
3080
  conn->lock();
3081

    
3082
  try
3083
  {
3084
    conn->begin();
3085
    //send sql statement and do error handling
3086
    QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
3087
    if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3088
      throw PGException( result );
3089
    returnvalue = conn->commit();
3090
    if ( mTransaction )
3091
      mTransaction->dirtyLastSavePoint();
3092
  }
3093
  catch ( PGException &e )
3094
  {
3095
    pushError( tr( "PostGIS error while renaming attributes: %1" ).arg( e.errorMessage() ) );
3096
    conn->rollback();
3097
    returnvalue = false;
3098
  }
3099

    
3100
  loadFields();
3101
  conn->unlock();
3102
  return returnvalue;
3103
}
3104

    
3105
bool QgsPostgresProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
3106
{
3107
  bool returnvalue = true;
3108

    
3109
  if ( mIsQuery )
3110
    return false;
3111

    
3112
  if ( attr_map.isEmpty() )
3113
    return true;
3114

    
3115
  QgsPostgresConn *conn = connectionRW();
3116
  if ( !conn )
3117
    return false;
3118

    
3119
  conn->lock();
3120

    
3121
  try
3122
  {
3123
    conn->begin();
3124

    
3125
    // cycle through the features
3126
    for ( QgsChangedAttributesMap::const_iterator iter = attr_map.constBegin(); iter != attr_map.constEnd(); ++iter )
3127
    {
3128
      QgsFeatureId fid = iter.key();
3129

    
3130
      // skip added features
3131
      if ( FID_IS_NEW( fid ) )
3132
        continue;
3133

    
3134
      const QgsAttributeMap &attrs = iter.value();
3135
      if ( attrs.isEmpty() )
3136
        continue;
3137

    
3138
      QString sql = QStringLiteral( "UPDATE %1 SET " ).arg( mQuery );
3139

    
3140
      bool pkChanged = false;
3141

    
3142
      // cycle through the changed attributes of the feature
3143
      QString delim;
3144
      int numChangedFields = 0;
3145
      for ( QgsAttributeMap::const_iterator siter = attrs.constBegin(); siter != attrs.constEnd(); ++siter )
3146
      {
3147
        try
3148
        {
3149
          QgsField fld = field( siter.key() );
3150

    
3151
          pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() );
3152

    
3153
          if ( mGeneratedValues.contains( siter.key() ) )
3154
          {
3155
            QgsLogger::warning( tr( "Changing the value of GENERATED field %1 is not allowed." ).arg( fld.name() ) );
3156
            continue;
3157
          }
3158

    
3159
          numChangedFields++;
3160

    
3161
          sql += delim + QStringLiteral( "%1=" ).arg( quotedIdentifier( fld.name() ) );
3162
          delim = ',';
3163

    
3164
          QString defVal = defaultValueClause( siter.key() );
3165
          if ( qgsVariantEqual( *siter, defVal ) )
3166
          {
3167
            sql += defVal.isNull() ? "NULL" : defVal;
3168
          }
3169
          else if ( fld.typeName() == QLatin1String( "geometry" ) )
3170
          {
3171
            QString val = geomAttrToString( siter.value(), connectionRO() );
3172

    
3173
            sql += QStringLiteral( "%1(%2)" )
3174
                     .arg( connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( val ) );
3175
          }
3176
          else if ( fld.typeName() == QLatin1String( "geography" ) )
3177
          {
3178
            sql += QStringLiteral( "st_geographyfromtext(%1)" )
3179
                     .arg( quotedValue( siter->toString() ) );
3180
          }
3181
          else if ( fld.typeName() == QLatin1String( "jsonb" ) )
3182
          {
3183
            sql += QStringLiteral( "%1::jsonb" )
3184
                     .arg( quotedJsonValue( siter.value() ) );
3185
          }
3186
          else if ( fld.typeName() == QLatin1String( "json" ) )
3187
          {
3188
            sql += QStringLiteral( "%1::json" )
3189
                     .arg( quotedJsonValue( siter.value() ) );
3190
          }
3191
          else if ( fld.typeName() == QLatin1String( "bytea" ) )
3192
          {
3193
            sql += quotedByteaValue( siter.value() );
3194
          }
3195
          else
3196
          {
3197
            sql += quotedValue( *siter );
3198
          }
3199
        }
3200
        catch ( PGFieldNotFound )
3201
        {
3202
          // Field was missing - shouldn't happen
3203
        }
3204
      }
3205

    
3206
      sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) );
3207

    
3208
      // Don't try to UPDATE an empty set of values (might happen if the table only has GENERATED fields,
3209
      // or if the user only changed GENERATED fields in the form/attribute table.
3210
      if ( numChangedFields > 0 )
3211
      {
3212
        QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
3213
        if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3214
          throw PGException( result );
3215
      }
3216
      else // let the user know that no field was actually changed
3217
      {
3218
        QgsLogger::warning( tr( "No fields were updated on the database." ) );
3219
      }
3220

    
3221
      // update feature id map if key was changed
3222
      // PktInt64 also uses a fid map even if it is a stand alone field.
3223
      if ( pkChanged && ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt64 ) )
3224
      {
3225
        QVariantList k = mShared->removeFid( fid );
3226

    
3227
        int keyCount = std::min( mPrimaryKeyAttrs.size(), k.size() );
3228

    
3229
        for ( int i = 0; i < keyCount; i++ )
3230
        {
3231
          int idx = mPrimaryKeyAttrs.at( i );
3232
          if ( !attrs.contains( idx ) )
3233
            continue;
3234

    
3235
          k[i] = attrs[idx];
3236
        }
3237

    
3238
        mShared->insertFid( fid, k );
3239
      }
3240
    }
3241

    
3242
    returnvalue &= conn->commit();
3243
    if ( mTransaction )
3244
      mTransaction->dirtyLastSavePoint();
3245
  }
3246
  catch ( PGException &e )
3247
  {
3248
    pushError( tr( "PostGIS error while changing attributes: %1" ).arg( e.errorMessage() ) );
3249
    conn->rollback();
3250
    returnvalue = false;
3251
  }
3252

    
3253
  conn->unlock();
3254
  return returnvalue;
3255
}
3256

    
3257
void QgsPostgresProvider::appendGeomParam( const QgsGeometry &geom, QStringList &params ) const
3258
{
3259
  if ( geom.isNull() )
3260
  {
3261
    params << QString();
3262
    return;
3263
  }
3264

    
3265
  QString param;
3266

    
3267
  const QgsGeometry convertedGeom( convertToProviderType( geom, wkbType() ) );
3268
  QByteArray wkb( !convertedGeom.isNull() ? convertedGeom.asWkb() : geom.asWkb() );
3269
  const unsigned char *buf = reinterpret_cast<const unsigned char *>( wkb.constData() );
3270
  int wkbSize = wkb.length();
3271

    
3272
  for ( int i = 0; i < wkbSize; ++i )
3273
  {
3274
    if ( connectionRO()->useWkbHex() )
3275
      param += QStringLiteral( "%1" ).arg( ( int ) buf[i], 2, 16, QChar( '0' ) );
3276
    else
3277
      param += QStringLiteral( "\\%1" ).arg( ( int ) buf[i], 3, 8, QChar( '0' ) );
3278
  }
3279
  params << param;
3280
}
3281

    
3282
bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
3283
{
3284
  if ( mIsQuery || mGeometryColumn.isNull() )
3285
    return false;
3286

    
3287
  QgsPostgresConn *conn = connectionRW();
3288
  if ( !conn )
3289
  {
3290
    return false;
3291
  }
3292
  conn->lock();
3293

    
3294
  bool returnvalue = true;
3295

    
3296
  try
3297
  {
3298
    // Start the PostGIS transaction
3299
    conn->begin();
3300

    
3301
    QString update;
3302
    QgsPostgresResult result;
3303

    
3304
    if ( mSpatialColType == SctTopoGeometry )
3305
    {
3306
      // We will create a new TopoGeometry object with the new shape.
3307
      // Later, we'll replace the old TopoGeometry with the new one,
3308
      // to avoid orphans and retain higher level in an eventual
3309
      // hierarchical definition
3310
      update = QStringLiteral( "SELECT id(%1) FROM %2 o WHERE %3" )
3311
                 .arg( geomParam( 1 ), mQuery, pkParamWhereClause( 2 ) );
3312

    
3313
      QString getid = QStringLiteral( "SELECT id(%1) FROM %2 WHERE %3" )
3314
                        .arg( quotedIdentifier( mGeometryColumn ), mQuery, pkParamWhereClause( 1 ) );
3315

    
3316
      QgsDebugMsgLevel( "getting old topogeometry id: " + getid, 2 );
3317

    
3318
      result = connectionRO()->PQprepare( QStringLiteral( "getid" ), getid, 1, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
3319
      if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3320
      {
3321
        QgsDebugError( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3322
                         .arg( result.PQresultStatus() )
3323
                         .arg( PGRES_COMMAND_OK )
3324
                         .arg( getid ) );
3325
        throw PGException( result );
3326
      }
3327

    
3328
      QString replace = QString( "UPDATE %1 SET %2="
3329
                                 "( topology_id(%2),layer_id(%2),$1,type(%2) )"
3330
                                 "WHERE %3" )
3331
                          .arg( mQuery, quotedIdentifier( mGeometryColumn ), pkParamWhereClause( 2 ) );
3332
      QgsDebugMsgLevel( "TopoGeom swap: " + replace, 2 );
3333
      result = conn->PQprepare( QStringLiteral( "replacetopogeom" ), replace, 2, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
3334
      if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3335
      {
3336
        QgsDebugError( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3337
                         .arg( result.PQresultStatus() )
3338
                         .arg( PGRES_COMMAND_OK )
3339
                         .arg( replace ) );
3340
        throw PGException( result );
3341
      }
3342
    }
3343
    else
3344
    {
3345
      update = QStringLiteral( "UPDATE %1 SET %2=%3 WHERE %4" )
3346
                 .arg( mQuery, quotedIdentifier( mGeometryColumn ), geomParam( 1 ), pkParamWhereClause( 2 ) );
3347
    }
3348

    
3349
    QgsDebugMsgLevel( "updating: " + update, 2 );
3350

    
3351
    result = conn->PQprepare( QStringLiteral( "updatefeatures" ), update, 2, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
3352
    if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3353
    {
3354
      QgsDebugError( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3355
                       .arg( result.PQresultStatus() )
3356
                       .arg( PGRES_COMMAND_OK )
3357
                       .arg( update ) );
3358
      throw PGException( result );
3359
    }
3360

    
3361
    QgsDebugMsgLevel( QStringLiteral( "iterating over the map of changed geometries..." ), 2 );
3362

    
3363
    for ( QgsGeometryMap::const_iterator iter = geometry_map.constBegin();
3364
          iter != geometry_map.constEnd();
3365
          ++iter )
3366
    {
3367
      QgsDebugMsgLevel( "iterating over feature id " + FID_TO_STRING( iter.key() ), 2 );
3368

    
3369
      // Save the id of the current topogeometry
3370
      long old_tg_id = -1;
3371
      if ( mSpatialColType == SctTopoGeometry )
3372
      {
3373
        QStringList params;
3374
        appendPkParams( iter.key(), params );
3375
        result = connectionRO()->PQexecPrepared( QStringLiteral( "getid" ), params, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
3376
        if ( result.PQresultStatus() != PGRES_TUPLES_OK )
3377
        {
3378
          QgsDebugError( QStringLiteral( "Exception thrown due to PQexecPrepared of 'getid' returning != PGRES_TUPLES_OK (%1 != expected %2)" )
3379
                           .arg( result.PQresultStatus() )
3380
                           .arg( PGRES_TUPLES_OK ) );
3381
          throw PGException( result );
3382
        }
3383
        // TODO: watch out for NULL, handle somehow
3384
        old_tg_id = result.PQgetvalue( 0, 0 ).toLong();
3385
        QgsDebugMsgLevel( QStringLiteral( "Old TG id is %1" ).arg( old_tg_id ), 2 );
3386
      }
3387

    
3388
      QStringList params;
3389
      appendGeomParam( *iter, params );
3390
      appendPkParams( iter.key(), params );
3391

    
3392
      result = conn->PQexecPrepared( QStringLiteral( "updatefeatures" ), params, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
3393
      if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3394
        throw PGException( result );
3395

    
3396
      if ( mSpatialColType == SctTopoGeometry )
3397
      {
3398
        long new_tg_id = result.PQgetvalue( 0, 0 ).toLong(); // new topogeo_id
3399

    
3400
        // Replace old TopoGeom with new TopoGeom, so that
3401
        // any hierarchically defined TopoGeom will still have its
3402
        // definition and we'll leave no orphans
3403
        QString replace = QString( "DELETE FROM %1.relation WHERE "
3404
                                   "layer_id = %2 AND topogeo_id = %3" )
3405
                            .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) )
3406
                            .arg( mTopoLayerInfo.layerId )
3407
                            .arg( old_tg_id );
3408
        result = conn->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), replace );
3409
        if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3410
        {
3411
          QgsDebugError( QStringLiteral( "Exception thrown due to PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3412
                           .arg( result.PQresultStatus() )
3413
                           .arg( PGRES_COMMAND_OK )
3414
                           .arg( replace ) );
3415
          throw PGException( result );
3416
        }
3417
        // TODO: use prepared query here
3418
        replace = QString( "UPDATE %1.relation SET topogeo_id = %2 "
3419
                           "WHERE layer_id = %3 AND topogeo_id = %4" )
3420
                    .arg( quotedIdentifier( mTopoLayerInfo.topologyName ) )
3421
                    .arg( old_tg_id )
3422
                    .arg( mTopoLayerInfo.layerId )
3423
                    .arg( new_tg_id );
3424
        QgsDebugMsgLevel( "relation swap: " + replace, 2 );
3425
        result = conn->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), replace );
3426
        if ( result.PQresultStatus() != PGRES_COMMAND_OK )
3427
        {
3428
          QgsDebugError( QStringLiteral( "Exception thrown due to PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3429
                           .arg( result.PQresultStatus() )
3430
                           .arg( PGRES_COMMAND_OK )
3431
                           .arg( replace ) );
3432
          throw PGException( result );
3433
        }
3434
      } // if TopoGeometry
3435

    
3436
    } // for each feature
3437

    
3438
    conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE updatefeatures" ) );
3439
    if ( mSpatialColType == SctTopoGeometry )
3440
    {
3441
      connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE getid" ) );
3442
      conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE replacetopogeom" ) );
3443
    }
3444

    
3445
    returnvalue &= conn->commit();
3446
    if ( mTransaction )
3447
      mTransaction->dirtyLastSavePoint();
3448
  }
3449
  catch ( PGException &e )
3450
  {
3451
    pushError( tr( "PostGIS error while changing geometry values: %1" ).arg( e.errorMessage() ) );
3452
    conn->rollback();
3453
    conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE updatefeatures" ) );
3454
    if ( mSpatialColType == SctTopoGeometry )
3455
    {
3456
      connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE getid" ) );
3457
      conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE replacetopogeom" ) );
3458
    }
3459
    returnvalue = false;
3460
  }
3461

    
3462
  conn->unlock();
3463

    
3464
  QgsDebugMsgLevel( QStringLiteral( "leaving." ), 4 );
3465

    
3466
  return returnvalue;
3467
}
3468

    
3469
bool QgsPostgresProvider::changeFeatures( const QgsChangedAttributesMap &attr_map, const QgsGeometryMap &geometry_map )
3470
{
3471
  Q_ASSERT( mSpatialColType != SctTopoGeometry );
3472

    
3473
  bool returnvalue = true;
3474

    
3475
  if ( mIsQuery )
3476
    return false;
3477

    
3478
  if ( attr_map.isEmpty() )
3479
    return true;
3480

    
3481
  QgsPostgresConn *conn = connectionRW();
3482
  if ( !conn )
3483
    return false;
3484

    
3485
  conn->lock();
3486

    
3487
  try
3488
  {
3489
    conn->begin();
3490

    
3491
    QgsFeatureIds ids( qgis::listToSet( attr_map.keys() ) );
3492
    ids |= qgis::listToSet( geometry_map.keys() );
3493

    
3494
    // cycle through the features
3495
    const auto constIds = ids;
3496
    for ( QgsFeatureId fid : constIds )
3497
    {
3498
      // skip added features
3499
      if ( FID_IS_NEW( fid ) )
3500
        continue;
3501

    
3502
      const QgsAttributeMap &attrs = attr_map.value( fid );
3503
      if ( attrs.isEmpty() && !geometry_map.contains( fid ) )
3504
        continue;
3505

    
3506
      QString sql = QStringLiteral( "UPDATE %1 SET " ).arg( mQuery );
3507

    
3508
      bool pkChanged = false;
3509

    
3510
      // cycle through the changed attributes of the feature
3511
      QString delim;
3512
      int numChangedFields = 0;
3513

    
3514
      for ( QgsAttributeMap::const_iterator siter = attrs.constBegin(); siter != attrs.constEnd(); ++siter )
3515
      {
3516
        try
3517
        {
3518
          QgsField fld = field( siter.key() );
3519

    
3520
          pkChanged = pkChanged || mPrimaryKeyAttrs.contains( siter.key() );
3521

    
3522
          if ( mGeneratedValues.contains( siter.key() ) )
3523
          {
3524
            QgsLogger::warning( tr( "Changing the value of GENERATED field %1 is not allowed." ).arg( fld.name() ) );
3525
            continue;
3526
          }
3527

    
3528
          numChangedFields++;
3529

    
3530
          sql += delim + QStringLiteral( "%1=" ).arg( quotedIdentifier( fld.name() ) );
3531
          delim = ',';
3532

    
3533
          if ( fld.typeName() == QLatin1String( "geometry" ) )
3534
          {
3535
            QString val = geomAttrToString( siter.value(), connectionRO() );
3536
            sql += QStringLiteral( "%1(%2)" )
3537
                     .arg( connectionRO()->majorVersion() < 2 ? "geomfromewkt" : "st_geomfromewkt", quotedValue( val ) );
3538
          }
3539
          else if ( fld.typeName() == QLatin1String( "geography" ) )
3540
          {
3541
            sql += QStringLiteral( "st_geographyfromtext(%1)" )
3542
                     .arg( quotedValue( siter->toString() ) );
3543
          }
3544
          else if ( fld.typeName() == QLatin1String( "jsonb" ) )
3545
          {
3546
            sql += QStringLiteral( "%1::jsonb" )
3547
                     .arg( quotedJsonValue( siter.value() ) );
3548
          }
3549
          else if ( fld.typeName() == QLatin1String( "json" ) )
3550
          {
3551
            sql += QStringLiteral( "%1::json" )
3552
                     .arg( quotedJsonValue( siter.value() ) );
3553
          }
3554
          else if ( fld.typeName() == QLatin1String( "bytea" ) )
3555
          {
3556
            sql += quotedByteaValue( siter.value() );
3557
          }
3558
          else
3559
          {
3560
            sql += quotedValue( *siter );
3561
          }
3562
        }
3563
        catch ( PGFieldNotFound )
3564
        {
3565
          // Field was missing - shouldn't happen
3566
        }
3567
      }
3568

    
3569
      if ( !geometry_map.contains( fid ) )
3570
      {
3571
        // Don't try to UPDATE an empty set of values (might happen if the table only has GENERATED fields,
3572
        // or if the user only changed GENERATED fields in the form/attribute table.
3573
        if ( numChangedFields > 0 )
3574
        {
3575
          sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) );
3576

    
3577
          QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
3578
          if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3579
            throw PGException( result );
3580
        }
3581
        else // let the user know that nothing has actually changed
3582
        {
3583
          QgsLogger::warning( tr( "No fields/geometries were updated on the database." ) );
3584
        }
3585
      }
3586
      else
3587
      {
3588
        sql += QStringLiteral( "%1%2=%3" ).arg( delim, quotedIdentifier( mGeometryColumn ), geomParam( 1 ) );
3589
        sql += QStringLiteral( " WHERE %1" ).arg( whereClause( fid ) );
3590

    
3591
        QgsPostgresResult result( conn->PQprepare( QStringLiteral( "updatefeature" ), sql, 1, nullptr, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN ) );
3592
        if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3593
        {
3594
          QgsDebugError( QStringLiteral( "Exception thrown due to PQprepare of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
3595
                           .arg( result.PQresultStatus() )
3596
                           .arg( PGRES_COMMAND_OK )
3597
                           .arg( sql ) );
3598
          throw PGException( result );
3599
        }
3600

    
3601
        QStringList params;
3602
        const QgsGeometry &geom = geometry_map[fid];
3603
        appendGeomParam( geom, params );
3604

    
3605
        result = conn->PQexecPrepared( QStringLiteral( "updatefeature" ), params, QStringLiteral( "QgsPostgresProvider" ), QGS_QUERY_LOG_ORIGIN );
3606
        if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK )
3607
        {
3608
          conn->rollback();
3609
          conn->PQexecNR( QStringLiteral( "DEALLOCATE updatefeature" ) );
3610
          throw PGException( result );
3611
        }
3612

    
3613
        conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "DEALLOCATE updatefeature" ) );
3614
      }
3615

    
3616
      // update feature id map if key was changed
3617
      // PktInt64 also uses a fid map even though it is a single field.
3618
      if ( pkChanged && ( mPrimaryKeyType == PktFidMap || mPrimaryKeyType == PktInt64 ) )
3619
      {
3620
        QVariantList k = mShared->removeFid( fid );
3621

    
3622
        for ( int i = 0; i < mPrimaryKeyAttrs.size(); i++ )
3623
        {
3624
          int idx = mPrimaryKeyAttrs.at( i );
3625
          if ( !attrs.contains( idx ) )
3626
            continue;
3627

    
3628
          k[i] = attrs[idx];
3629
        }
3630

    
3631
        mShared->insertFid( fid, k );
3632
      }
3633
    }
3634

    
3635
    returnvalue &= conn->commit();
3636
    if ( mTransaction )
3637
      mTransaction->dirtyLastSavePoint();
3638
  }
3639
  catch ( PGException &e )
3640
  {
3641
    pushError( tr( "PostGIS error while changing attributes: %1" ).arg( e.errorMessage() ) );
3642
    conn->rollback();
3643
    returnvalue = false;
3644
  }
3645

    
3646
  conn->unlock();
3647

    
3648
  QgsDebugMsgLevel( QStringLiteral( "leaving." ), 4 );
3649

    
3650
  return returnvalue;
3651
}
3652

    
3653
QgsAttributeList QgsPostgresProvider::attributeIndexes() const
3654
{
3655
  QgsAttributeList lst;
3656
  lst.reserve( mAttributeFields.count() );
3657
  for ( int i = 0; i < mAttributeFields.count(); ++i )
3658
    lst.append( i );
3659
  return lst;
3660
}
3661

    
3662
Qgis::VectorProviderCapabilities QgsPostgresProvider::capabilities() const
3663
{
3664
  return mEnabledCapabilities;
3665
}
3666

    
3667
Qgis::VectorDataProviderAttributeEditCapabilities QgsPostgresProvider::attributeEditCapabilities() const
3668
{
3669
  return Qgis::VectorDataProviderAttributeEditCapability::EditComment;
3670
}
3671

    
3672
Qgis::SpatialIndexPresence QgsPostgresProvider::hasSpatialIndex() const
3673
{
3674
  QgsPostgresProviderConnection conn( mUri.uri(), QVariantMap() );
3675
  try
3676
  {
3677
    return conn.spatialIndexExists( mUri.schema(), mUri.table(), mUri.geometryColumn() ) ? Qgis::SpatialIndexPresence::Present : Qgis::SpatialIndexPresence::NotPresent;
3678
  }
3679
  catch ( QgsProviderConnectionException & )
3680
  {
3681
    return Qgis::SpatialIndexPresence::Unknown;
3682
  }
3683
}
3684

    
3685
bool QgsPostgresProvider::setSubsetString( const QString &theSQL, bool updateFeatureCount )
3686
{
3687
  if ( theSQL.trimmed() == mSqlWhereClause )
3688
    return true;
3689

    
3690
  QString prevWhere = mSqlWhereClause;
3691

    
3692
  mSqlWhereClause = theSQL.trimmed();
3693

    
3694
  QString sql = QStringLiteral( "SELECT * FROM %1" ).arg( mQuery );
3695

    
3696
  if ( !mSqlWhereClause.isEmpty() )
3697
  {
3698
    sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
3699
  }
3700

    
3701
  sql += QLatin1String( " LIMIT 0" );
3702

    
3703
  QgsPostgresResult res( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
3704
  if ( res.PQresultStatus() != PGRES_TUPLES_OK )
3705
  {
3706
    pushError( res.PQresultErrorMessage() );
3707
    mSqlWhereClause = prevWhere;
3708
    return false;
3709
  }
3710

    
3711
#if 0
3712
  // FIXME
3713
  if ( mPrimaryKeyType == PktInt && !uniqueData( primaryKeyAttr ) )
3714
  {
3715
    sqlWhereClause = prevWhere;
3716
    return false;
3717
  }
3718
#endif
3719

    
3720
  // Update datasource uri too
3721
  mUri.setSql( theSQL );
3722
  // Update yet another copy of the uri. Why are there 3 copies of the
3723
  // uri? Perhaps this needs some rationalisation.....
3724
  setDataSourceUri( mUri.uri( false ) );
3725

    
3726
  if ( updateFeatureCount )
3727
  {
3728
    reloadData();
3729
  }
3730
  else
3731
  {
3732
    mLayerExtent.reset();
3733
    emit dataChanged();
3734
  }
3735

    
3736
  return true;
3737
}
3738

    
3739
bool QgsPostgresProvider::supportsSubsetString() const
3740
{
3741
  return true;
3742
}
3743

    
3744
QString QgsPostgresProvider::subsetStringDialect() const
3745
{
3746
  return tr( "PostgreSQL WHERE clause" );
3747
}
3748

    
3749
QString QgsPostgresProvider::subsetStringHelpUrl() const
3750
{
3751
  return QStringLiteral( "https://www.postgresql.org/docs/current/sql-expressions.html" );
3752
}
3753

    
3754
long long QgsPostgresProvider::featureCount() const
3755
{
3756
  long long featuresCounted = mShared->featuresCounted();
3757
  if ( featuresCounted >= 0 )
3758
    return featuresCounted;
3759

    
3760
  // See: https://github.com/qgis/QGIS/issues/25285 - QGIS crashes on featureCount())
3761
  if ( !connectionRO() )
3762
  {
3763
    return 0;
3764
  }
3765

    
3766
  // get total number of features
3767
  QString sql;
3768

    
3769
  long long num = -1;
3770
  if ( !mIsQuery && mUseEstimatedMetadata )
3771
  {
3772
    if ( ( relkind() == Qgis::PostgresRelKind::View || !mSqlWhereClause.isEmpty() ) && connectionRO()->pgVersion() >= 90000 )
3773
    {
3774
      // parse explain output to estimate feature count
3775
      // we don't use pg_class reltuples because it returns 0 for view
3776
      sql = QStringLiteral( "EXPLAIN (FORMAT JSON) SELECT 1 FROM %1%2" ).arg( mQuery, filterWhereClause() );
3777
      QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
3778

    
3779
      const QString json = result.PQgetvalue( 0, 0 );
3780
      const QVariantList explain = QgsJsonUtils::parseJson( json ).toList();
3781
      const QVariantMap countPlan = !explain.isEmpty() ? explain[0].toMap().value( "Plan" ).toMap() : QVariantMap();
3782
      const QVariant nbRows = countPlan.value( "Plan Rows" );
3783

    
3784
      if ( nbRows.isValid() )
3785
        num = nbRows.toLongLong();
3786
      else
3787
        QgsLogger::warning( QStringLiteral( "Cannot parse JSON explain result to estimate feature count (%1) : %2" ).arg( sql, json ) );
3788
    }
3789
    else
3790
    {
3791
      sql = QStringLiteral( "SELECT reltuples::bigint FROM pg_catalog.pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) );
3792
      QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
3793
      num = result.PQgetvalue( 0, 0 ).toLongLong();
3794
    }
3795
  }
3796
  else
3797
  {
3798
    sql = QStringLiteral( "SELECT count(*) FROM %1%2" ).arg( mQuery, filterWhereClause() );
3799
    QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
3800

    
3801
    QgsDebugMsgLevel( "number of features as text: " + result.PQgetvalue( 0, 0 ), 2 );
3802

    
3803
    num = result.PQgetvalue( 0, 0 ).toLongLong();
3804
  }
3805

    
3806
  mShared->setFeaturesCounted( num );
3807

    
3808
  QgsDebugMsgLevel( "number of features: " + QString::number( num ), 2 );
3809

    
3810
  return num;
3811
}
3812

    
3813
bool QgsPostgresProvider::empty() const
3814
{
3815
  QString sql = QStringLiteral( "SELECT EXISTS (SELECT * FROM %1%2 LIMIT 1)" ).arg( mQuery, filterWhereClause() );
3816
  QgsPostgresResult res( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
3817
  if ( res.PQresultStatus() != PGRES_TUPLES_OK )
3818
  {
3819
    pushError( res.PQresultErrorMessage() );
3820
    return false;
3821
  }
3822

    
3823
  return res.PQgetvalue( 0, 0 ) != QLatin1String( "t" );
3824
}
3825

    
3826
QgsRectangle QgsPostgresProvider::extent() const
3827
{
3828
  return extent3D().toRectangle();
3829
}
3830

    
3831
bool QgsPostgresProvider::estimateExtent() const
3832
{
3833
  // Cannot estimate extent of a query
3834
  if ( mIsQuery )
3835
  {
3836
    QgsDebugMsgLevel( "Estimating extent of queries is not supported", 2 );
3837
    return false;
3838
  }
3839

    
3840
  const int vmaj = connectionRO()->majorVersion();
3841
  const int vmin = connectionRO()->minorVersion();
3842

    
3843
  if ( mSpatialColType == SctGeography )
3844
  {
3845
    // PostGIS up to PostGIS-3.4.x had bogus estimation
3846
    // for geography type, https://trac.osgeo.org/postgis/ticket/5734
3847
    if ( vmaj < 3 || ( vmaj == 3 && vmin < 5 ) )
3848
    {
3849
      QgsDebugMsgLevel( QStringLiteral( "Estimating extent of geography columns was not supported by PostGIS %1.%2 (3.5+ required)" ).arg( vmaj, vmin ), 2 );
3850
      return false;
3851
    }
3852
  }
3853

    
3854
  QString sql = QStringLiteral( "SELECT %1(%2,%3,%4)" )
3855
                  .arg(
3856
                    vmaj < 2 ? "estimated_extent" : ( vmaj == 2 && vmin < 1 ? "st_estimated_extent" : "st_estimatedextent" ),
3857
                    quotedValue( mSchemaName ),
3858
                    quotedValue( mTableName ),
3859
                    quotedValue( mGeometryColumn )
3860
                  );
3861

    
3862
  QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
3863

    
3864
  if ( result.PQresultStatus() != PGRES_TUPLES_OK )
3865
  {
3866
    pushError( result.PQresultErrorMessage() );
3867
    return false;
3868
  }
3869
  if ( result.PQntuples() != 1 )
3870
  {
3871
    pushError( tr( "Unexpected number of tuples from estimated extent query %1: %2 (1 expected)." )
3872
                 .arg( sql )
3873
                 .arg( result.PQntuples() ) );
3874
    return false;
3875
  }
3876

    
3877
  if ( result.PQgetisnull( 0, 0 ) )
3878
    return false;
3879

    
3880
  QString box2dString = result.PQgetvalue( 0, 0 );
3881

    
3882
  QgsDebugMsgLevel( QStringLiteral( "Got estimated extent (%1) using: %2" ).arg( box2dString ).arg( sql ), 2 );
3883

    
3884
  const thread_local QRegularExpression rx2d( "\\((.+) (.+),(.+) (.+)\\)" );
3885
  const QRegularExpressionMatch match = rx2d.match( box2dString );
3886
  if ( !match.hasMatch() )
3887
  {
3888
    pushError( tr( "Unexpected format from estimated extent query %1: %2." ).arg( sql, box2dString ) );
3889
    return false; // throw instead ?
3890
  }
3891

    
3892
  mLayerExtent.emplace(
3893
    match.captured( 1 ).toDouble(),           // xmin
3894
    match.captured( 2 ).toDouble(),           // ymin
3895
    std::numeric_limits<double>::quiet_NaN(), // zmin
3896
    match.captured( 3 ).toDouble(),           // xmax
3897
    match.captured( 4 ).toDouble(),           // ymax
3898
    std::numeric_limits<double>::quiet_NaN()  // zmax
3899
  );
3900

    
3901
  QgsDebugMsgLevel( "Set extents to estimated value: " + mLayerExtent->toString(), 2 );
3902
  return true;
3903
}
3904

    
3905
bool QgsPostgresProvider::computeExtent3D() const
3906
{
3907
  QString sql = QStringLiteral( "SELECT %1(%2%3) FROM %4%5" )
3908
                  .arg( connectionRO()->majorVersion() < 2 ? "extent" : "ST_3DExtent", quotedIdentifier( mBoundingBoxColumn ), ( mSpatialColType == SctPcPatch || mSpatialColType == SctGeography ) ? "::geometry" : "", mQuery, filterWhereClause() );
3909

    
3910
  QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
3911

    
3912
  if ( result.PQresultStatus() != PGRES_TUPLES_OK )
3913
  {
3914
    pushError( result.PQresultErrorMessage() );
3915
    return false;
3916
  }
3917

    
3918
  if ( result.PQntuples() != 1 )
3919
  {
3920
    pushError( tr( "Unexpected number of tuples from compute extent query %1: %2 (1 expected)." )
3921
                 .arg( sql )
3922
                 .arg( result.PQntuples() ) );
3923
    return false;
3924
  }
3925

    
3926
  if ( result.PQgetisnull( 0, 0 ) )
3927
  {
3928
    // Layer is empty, set layerExtent to null (default-construct)
3929
    QgsDebugMsgLevel( QStringLiteral( "Got null from extent aggregate, setting layer extent to null as well" ), 2 );
3930
    mLayerExtent.emplace(); // constructs a NULL
3931
    return true;
3932
  }
3933

    
3934
  QString ext = result.PQgetvalue( 0, 0 );
3935

    
3936
  if ( ext.isEmpty() )
3937
  {
3938
    pushError( tr( "Unexpected empty result from extent query %1." ).arg( sql ) );
3939
    return false;
3940
  }
3941

    
3942
  QgsDebugMsgLevel( QStringLiteral( "Got extents (%1) using: %2" ).arg( ext ).arg( sql ), 2 );
3943

    
3944
  // Try the BOX3D format
3945
  const thread_local QRegularExpression rx3d( "\\((.+) (.+) (.+),(.+) (.+) (.+)\\)" );
3946
  QRegularExpressionMatch match = rx3d.match( ext );
3947
  if ( match.hasMatch() )
3948
  {
3949
    mLayerExtent.emplace(
3950
      match.captured( 1 ).toDouble(), // xmin
3951
      match.captured( 2 ).toDouble(), // ymin
3952
      match.captured( 3 ).toDouble(), // zmin
3953
      match.captured( 4 ).toDouble(), // xmax
3954
      match.captured( 5 ).toDouble(), // ymax
3955
      match.captured( 6 ).toDouble()  // zmax
3956
    );
3957
    QgsDebugMsgLevel( "Set extents to computed 3D value: " + mLayerExtent->toString(), 2 );
3958
    if ( !elevationProperties()->containsElevationData() )
3959
    {
3960
      // TODO: add a QgsBox3D::force2D method
3961
      mLayerExtent->setZMinimum( std::numeric_limits<double>::quiet_NaN() );
3962
      mLayerExtent->setZMaximum( std::numeric_limits<double>::quiet_NaN() );
3963
      QgsDebugMsgLevel( "Removed Z from extent as layer is configured to not have elevation properties", 2 );
3964
    }
3965
    return true;
3966
  }
3967

    
3968
  // Try the BOX2D format
3969
  const thread_local QRegularExpression rx2d( "\\((.+) (.+),(.+) (.+)\\)" );
3970
  match = rx2d.match( ext );
3971
  if ( match.hasMatch() )
3972
  {
3973
    mLayerExtent.emplace(
3974
      match.captured( 1 ).toDouble(),           // xmin
3975
      match.captured( 2 ).toDouble(),           // ymin
3976
      std::numeric_limits<double>::quiet_NaN(), // zmin
3977
      match.captured( 3 ).toDouble(),           // xmax
3978
      match.captured( 4 ).toDouble(),           // ymax
3979
      std::numeric_limits<double>::quiet_NaN()  // zmax
3980
    );
3981
    QgsDebugMsgLevel( "Set extents to computed 2D value: " + mLayerExtent->toString(), 2 );
3982
    return true;
3983
  }
3984

    
3985
  QgsMessageLog::logMessage( tr( "Unexpected result from extent query %1: %2" ).arg( sql, ext ), tr( "PostGIS" ) );
3986
  return false;
3987
}
3988

    
3989
QgsBox3D QgsPostgresProvider::extent3D() const
3990
{
3991
  if ( !isValid() || mGeometryColumn.isNull() )
3992
    return QgsBox3D();
3993

    
3994
  if ( mLayerExtent.has_value() )
3995
    return *mLayerExtent;
3996

    
3997
  // Return the estimated extents, if requested and possible
3998
  if ( mUseEstimatedMetadata )
3999
    estimateExtent();
4000

    
4001
  // Compute the extents, if estimation failed or was disabled
4002
  if ( !mLayerExtent.has_value() )
4003
    computeExtent3D();
4004

    
4005
  if ( mLayerExtent.has_value() )
4006
  {
4007
    return *mLayerExtent;
4008
  }
4009
  else
4010
  {
4011
    pushError( tr( "Could not extract layer extent" ) );
4012
    return QgsBox3D();
4013
  }
4014
}
4015

    
4016
void QgsPostgresProvider::updateExtents()
4017
{
4018
  mLayerExtent.reset();
4019
}
4020

    
4021
bool QgsPostgresProvider::getGeometryDetails()
4022
{
4023
  if ( mGeometryColumn.isNull() )
4024
  {
4025
    mDetectedGeomType = Qgis::WkbType::NoGeometry;
4026
    mValid = true;
4027
    return true;
4028
  }
4029

    
4030
  QgsPostgresResult result;
4031
  QString sql;
4032

    
4033
  QString schemaName = mSchemaName;
4034
  QString tableName = mTableName;
4035
  QString geomCol = mGeometryColumn;
4036
  QString geomColType;
4037

    
4038
  // Trust the datasource config means that we used requested geometry type and srid
4039
  // We only need to get the spatial column type
4040
  if ( ( mReadFlags & Qgis::DataProviderReadFlag::TrustDataSource ) && mRequestedGeomType != Qgis::WkbType::Unknown && !mRequestedSrid.isEmpty() )
4041
  {
4042
    if ( mIsQuery )
4043
    {
4044
      sql = QStringLiteral(
4045
              "SELECT t.typname FROM pg_type t inner join (SELECT pg_typeof(%1) typeof FROM %2 LIMIT 1) g ON oid = g.typeof"
4046
      )
4047
              .arg( quotedIdentifier( geomCol ), mQuery );
4048
    }
4049
    else
4050
    {
4051
      sql = QStringLiteral(
4052
              "SELECT t.typname FROM pg_type t inner join (SELECT pg_typeof(%1) typeof FROM %2.%3 LIMIT 1) g ON oid = g.typeof"
4053
      )
4054
              .arg( quotedIdentifier( geomCol ), quotedIdentifier( schemaName ), quotedIdentifier( tableName ) );
4055
    }
4056
    QgsDebugMsgLevel( QStringLiteral( "Getting the spatial column type: %1" ).arg( sql ), 2 );
4057

    
4058
    result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
4059
    if ( PGRES_TUPLES_OK == result.PQresultStatus() )
4060
    {
4061
      geomColType = result.PQgetvalue( 0, 0 );
4062

    
4063
      // Get spatial col type
4064
      if ( geomColType == QLatin1String( "geometry" ) )
4065
        mSpatialColType = SctGeometry;
4066
      else if ( geomColType == QLatin1String( "geography" ) )
4067
        mSpatialColType = SctGeography;
4068
      else if ( geomColType == QLatin1String( "topogeometry" ) )
4069
        mSpatialColType = SctTopoGeometry;
4070
      else if ( geomColType == QLatin1String( "pcpatch" ) )
4071
        mSpatialColType = SctPcPatch;
4072
      else
4073
        mSpatialColType = SctNone;
4074

    
4075
      // Use requested geometry type and srid
4076
      mDetectedGeomType = mRequestedGeomType;
4077
      mDetectedSrid = mRequestedSrid;
4078
      mValid = true;
4079
      return true;
4080
    }
4081
    else
4082
    {
4083
      mValid = false;
4084
      return false;
4085
    }
4086
  }
4087

    
4088
  if ( mIsQuery )
4089
  {
4090
    sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery );
4091

    
4092
    QgsDebugMsgLevel( QStringLiteral( "Getting geometry column: %1" ).arg( sql ), 2 );
4093

    
4094
    QgsPostgresResult result( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
4095
    if ( PGRES_TUPLES_OK == result.PQresultStatus() )
4096
    {
4097
      Oid tableoid = result.PQftable( 0 );
4098
      int column = result.PQftablecol( 0 );
4099

    
4100
      result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
4101
      if ( tableoid > 0 && PGRES_TUPLES_OK == result.PQresultStatus() )
4102
      {
4103
        sql = QStringLiteral( "SELECT pg_namespace.nspname,pg_class.relname FROM pg_class,pg_namespace WHERE pg_class.relnamespace=pg_namespace.oid AND pg_class.oid=%1" ).arg( tableoid );
4104
        result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
4105

    
4106
        if ( PGRES_TUPLES_OK == result.PQresultStatus() && 1 == result.PQntuples() )
4107
        {
4108
          schemaName = result.PQgetvalue( 0, 0 );
4109
          tableName = result.PQgetvalue( 0, 1 );
4110

    
4111
          sql = QStringLiteral( "SELECT a.attname, t.typname FROM pg_attribute a, pg_type t WHERE a.attrelid=%1 AND a.attnum=%2 AND a.atttypid = t.oid" ).arg( tableoid ).arg( column );
4112
          result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
4113
          if ( PGRES_TUPLES_OK == result.PQresultStatus() && 1 == result.PQntuples() )
4114
          {
4115
            geomCol = result.PQgetvalue( 0, 0 );
4116
            geomColType = result.PQgetvalue( 0, 1 );
4117
            if ( geomColType == QLatin1String( "geometry" ) )
4118
              mSpatialColType = SctGeometry;
4119
            else if ( geomColType == QLatin1String( "geography" ) )
4120
              mSpatialColType = SctGeography;
4121
            else if ( geomColType == QLatin1String( "topogeometry" ) )
4122
              mSpatialColType = SctTopoGeometry;
4123
            else if ( geomColType == QLatin1String( "pcpatch" ) )
4124
              mSpatialColType = SctPcPatch;
4125
            else
4126
              mSpatialColType = SctNone;
4127
          }
4128
          else
4129
          {
4130
            schemaName = mSchemaName;
4131
            tableName = mTableName;
4132
          }
4133
        }
4134
      }
4135
      else
4136
      {
4137
        schemaName.clear();
4138
        tableName = mQuery;
4139
      }
4140
    }
4141
    else
4142
    {
4143
      mValid = false;
4144
      return false;
4145
    }
4146
  }
4147

    
4148
  QString detectedType;
4149
  QString detectedSrid;
4150
  if ( !schemaName.isEmpty() )
4151
  {
4152
    // check geometry columns
4153
    sql = QStringLiteral( "SELECT upper(type),srid,coord_dimension FROM geometry_columns WHERE f_table_name=%1 AND f_geometry_column=%2 AND f_table_schema=%3" )
4154
            .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) );
4155

    
4156
    QgsDebugMsgLevel( QStringLiteral( "Getting geometry column: %1" ).arg( sql ), 2 );
4157
    result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
4158
    QgsDebugMsgLevel( QStringLiteral( "Geometry column query returned %1 rows" ).arg( result.PQntuples() ), 2 );
4159

    
4160
    if ( result.PQntuples() == 1 )
4161
    {
4162
      detectedType = result.PQgetvalue( 0, 0 );
4163

    
4164
      // Do not override the SRID if set in the data source URI
4165
      if ( detectedSrid.isEmpty() )
4166
      {
4167
        detectedSrid = result.PQgetvalue( 0, 1 );
4168
      }
4169

    
4170
      QString dim = result.PQgetvalue( 0, 2 );
4171
      if ( dim == QLatin1String( "3" ) && !detectedType.endsWith( 'M' ) )
4172
        detectedType += QLatin1Char( 'Z' );
4173
      else if ( dim == QLatin1String( "4" ) )
4174
        detectedType += QLatin1String( "ZM" );
4175

    
4176
      QString ds = result.PQgetvalue( 0, 1 );
4177
      if ( ds != QLatin1String( "0" ) )
4178
        detectedSrid = ds;
4179
      mSpatialColType = SctGeometry;
4180
    }
4181
    else
4182
    {
4183
      connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
4184
    }
4185

    
4186
    if ( detectedType.isEmpty() )
4187
    {
4188
      // check geography columns
4189
      sql = QStringLiteral( "SELECT upper(type),srid FROM geography_columns WHERE f_table_name=%1 AND f_geography_column=%2 AND f_table_schema=%3" )
4190
              .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) );
4191

    
4192
      QgsDebugMsgLevel( QStringLiteral( "Getting geography column: %1" ).arg( sql ), 2 );
4193
      result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
4194
      QgsDebugMsgLevel( QStringLiteral( "Geography column query returned %1" ).arg( result.PQntuples() ), 2 );
4195

    
4196
      if ( result.PQntuples() == 1 )
4197
      {
4198
        QString dt = result.PQgetvalue( 0, 0 );
4199
        if ( dt != "GEOMETRY" )
4200
          detectedType = dt;
4201
        QString ds = result.PQgetvalue( 0, 1 );
4202
        if ( ds != "0" )
4203
          detectedSrid = ds;
4204
        mSpatialColType = SctGeography;
4205
      }
4206
      else
4207
      {
4208
        connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
4209
      }
4210
    }
4211

    
4212
    if ( detectedType.isEmpty() && connectionRO()->hasTopology() )
4213
    {
4214
      // check topology.layer
4215
      sql = QString( "SELECT CASE "
4216
                     "WHEN l.feature_type = 1 THEN 'MULTIPOINT' "
4217
                     "WHEN l.feature_type = 2 THEN 'MULTILINESTRING' "
4218
                     "WHEN l.feature_type = 3 THEN 'MULTIPOLYGON' "
4219
                     "WHEN l.feature_type = 4 THEN 'GEOMETRYCOLLECTION' "
4220
                     "END AS type, t.srid FROM topology.layer l, topology.topology t "
4221
                     "WHERE l.topology_id = t.id AND l.schema_name=%3 "
4222
                     "AND l.table_name=%1 AND l.feature_column=%2" )
4223
              .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) );
4224

    
4225
      QgsDebugMsgLevel( QStringLiteral( "Getting TopoGeometry column: %1" ).arg( sql ), 2 );
4226
      result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
4227
      QgsDebugMsgLevel( QStringLiteral( "TopoGeometry column query returned %1" ).arg( result.PQntuples() ), 2 );
4228

    
4229
      if ( result.PQntuples() == 1 )
4230
      {
4231
        detectedType = result.PQgetvalue( 0, 0 );
4232
        detectedSrid = result.PQgetvalue( 0, 1 );
4233
        mSpatialColType = SctTopoGeometry;
4234
      }
4235
      else
4236
      {
4237
        connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
4238
      }
4239
    }
4240

    
4241
    if ( detectedType.isEmpty() && connectionRO()->hasPointcloud() )
4242
    {
4243
      // check pointcloud columns
4244
      sql = QStringLiteral( "SELECT 'POLYGON',srid FROM pointcloud_columns WHERE \"table\"=%1 AND \"column\"=%2 AND \"schema\"=%3" )
4245
              .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) );
4246

    
4247
      QgsDebugMsgLevel( QStringLiteral( "Getting pointcloud column: %1" ).arg( sql ), 2 );
4248
      result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
4249
      QgsDebugMsgLevel( QStringLiteral( "Pointcloud column query returned %1" ).arg( result.PQntuples() ), 2 );
4250

    
4251
      if ( result.PQntuples() == 1 )
4252
      {
4253
        detectedType = result.PQgetvalue( 0, 0 );
4254
        detectedSrid = result.PQgetvalue( 0, 1 );
4255
        mSpatialColType = SctPcPatch;
4256
      }
4257
      else
4258
      {
4259
        connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
4260
      }
4261
    }
4262

    
4263
    if ( mSpatialColType == SctNone )
4264
    {
4265
      sql = QString( "SELECT t.typname FROM "
4266
                     "pg_attribute a, pg_class c, pg_namespace n, pg_type t "
4267
                     "WHERE a.attrelid=c.oid AND c.relnamespace=n.oid "
4268
                     "AND a.atttypid=t.oid "
4269
                     "AND n.nspname=%3 AND c.relname=%1 AND a.attname=%2" )
4270
              .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) );
4271
      QgsDebugMsgLevel( QStringLiteral( "Getting column datatype: %1" ).arg( sql ), 2 );
4272
      result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
4273
      QgsDebugMsgLevel( QStringLiteral( "Column datatype query returned %1" ).arg( result.PQntuples() ), 2 );
4274
      if ( result.PQntuples() == 1 )
4275
      {
4276
        geomColType = result.PQgetvalue( 0, 0 );
4277
        if ( geomColType == QLatin1String( "geometry" ) )
4278
          mSpatialColType = SctGeometry;
4279
        else if ( geomColType == QLatin1String( "geography" ) )
4280
          mSpatialColType = SctGeography;
4281
        else if ( geomColType == QLatin1String( "topogeometry" ) )
4282
          mSpatialColType = SctTopoGeometry;
4283
        else if ( geomColType == QLatin1String( "pcpatch" ) )
4284
          mSpatialColType = SctPcPatch;
4285
      }
4286
      else
4287
      {
4288
        connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
4289
      }
4290
    }
4291
  }
4292
  else
4293
  {
4294
    sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery );
4295
    result = connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql );
4296
    if ( PGRES_TUPLES_OK == result.PQresultStatus() )
4297
    {
4298
      sql = QStringLiteral( "SELECT (SELECT t.typname FROM pg_type t WHERE oid = %1), upper(postgis_typmod_type(%2)), postgis_typmod_srid(%2)" )
4299
              .arg( QString::number( result.PQftype( 0 ) ), QString::number( result.PQfmod( 0 ) ) );
4300
      result = connectionRO()->LoggedPQexecNoLogError( "QgsPostgresProvider", sql );
4301
      if ( result.PQntuples() == 1 )
4302
      {
4303
        geomColType = result.PQgetvalue( 0, 0 );
4304
        detectedType = result.PQgetvalue( 0, 1 );
4305
        detectedSrid = result.PQgetvalue( 0, 2 );
4306
        if ( geomColType == QLatin1String( "geometry" ) )
4307
          mSpatialColType = SctGeometry;
4308
        else if ( geomColType == QLatin1String( "geography" ) )
4309
          mSpatialColType = SctGeography;
4310
        else if ( geomColType == QLatin1String( "topogeometry" ) )
4311
          mSpatialColType = SctTopoGeometry;
4312
        else if ( geomColType == QLatin1String( "pcpatch" ) )
4313
          mSpatialColType = SctPcPatch;
4314
        else
4315
        {
4316
          detectedType = mRequestedGeomType == Qgis::WkbType::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType );
4317
          detectedSrid = mRequestedSrid;
4318
        }
4319
      }
4320
      else
4321
      {
4322
        connectionRO()->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
4323
        detectedType = mRequestedGeomType == Qgis::WkbType::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType );
4324
      }
4325
    }
4326
    else
4327
    {
4328
      mValid = false;
4329
      return false;
4330
    }
4331
  }
4332

    
4333
  mDetectedGeomType = QgsPostgresConn::wkbTypeFromPostgis( detectedType );
4334
  mDetectedSrid = detectedSrid;
4335

    
4336
  if ( mDetectedGeomType == Qgis::WkbType::Unknown )
4337
  {
4338
    QgsPostgresLayerProperty layerProperty;
4339
    if ( !mIsQuery )
4340
    {
4341
      layerProperty.schemaName = schemaName;
4342
      layerProperty.tableName = tableName;
4343
    }
4344
    else
4345
    {
4346
      layerProperty.schemaName.clear();
4347
      layerProperty.tableName = mQuery;
4348
    }
4349
    layerProperty.geometryColName = mGeometryColumn;
4350
    layerProperty.geometryColType = mSpatialColType;
4351

    
4352
    QString delim;
4353

    
4354
    if ( !mSqlWhereClause.isEmpty() )
4355
    {
4356
      layerProperty.sql += delim + '(' + mSqlWhereClause + ')';
4357
      delim = QStringLiteral( " AND " );
4358
    }
4359

    
4360
    connectionRO()->retrieveLayerTypes( layerProperty, mUseEstimatedMetadata );
4361

    
4362
    mSpatialColType = layerProperty.geometryColType;
4363

    
4364
    if ( layerProperty.size() == 0 )
4365
    {
4366
      // no data - so take what's requested/detected
4367
      if ( mRequestedGeomType == Qgis::WkbType::Unknown || mDetectedSrid.isEmpty() )
4368
      {
4369
        QgsMessageLog::logMessage( tr( "Geometry type and srid for empty column %1 of %2 undefined." ).arg( mGeometryColumn, mQuery ) );
4370
      }
4371
    }
4372
    else
4373
    {
4374
      int i;
4375
      for ( i = 0; i < layerProperty.size(); i++ )
4376
      {
4377
        Qgis::WkbType wkbType = layerProperty.types.at( i );
4378

    
4379
        if ( ( wkbType != Qgis::WkbType::Unknown && ( mRequestedGeomType == Qgis::WkbType::Unknown || mRequestedGeomType == wkbType ) ) && ( mRequestedSrid.isEmpty() || layerProperty.srids.at( i ) == mRequestedSrid.toInt() ) )
4380
          break;
4381
      }
4382

    
4383
      // requested type && srid is available
4384
      if ( i < layerProperty.size() )
4385
      {
4386
        if ( layerProperty.size() == 1 )
4387
        {
4388
          // only what we requested is available
4389
          mDetectedGeomType = layerProperty.types.at( 0 );
4390
          mDetectedSrid = QString::number( layerProperty.srids.at( 0 ) );
4391
        }
4392
      }
4393
      else
4394
      {
4395
        // geometry type undetermined or not unrequested
4396
        QgsMessageLog::logMessage( tr( "Feature type or srid for %1 of %2 could not be determined or was not requested." ).arg( mGeometryColumn, mQuery ) );
4397
      }
4398
    }
4399
  }
4400

    
4401
  QgsDebugMsgLevel( QStringLiteral( "Detected SRID is %1" ).arg( mDetectedSrid ), 2 );
4402
  QgsDebugMsgLevel( QStringLiteral( "Requested SRID is %1" ).arg( mRequestedSrid ), 2 );
4403
  QgsDebugMsgLevel( QStringLiteral( "Detected type is %1" ).arg( qgsEnumValueToKey( mDetectedGeomType ) ), 2 );
4404
  QgsDebugMsgLevel( QStringLiteral( "Requested type is %1" ).arg( qgsEnumValueToKey( mRequestedGeomType ) ), 2 );
4405

    
4406
  mValid = ( mDetectedGeomType != Qgis::WkbType::Unknown || mRequestedGeomType != Qgis::WkbType::Unknown )
4407
           && ( !mDetectedSrid.isEmpty() || !mRequestedSrid.isEmpty() );
4408

    
4409
  if ( !mValid )
4410
    return false;
4411

    
4412
  QgsDebugMsgLevel( QStringLiteral( "Spatial column type is %1" ).arg( QgsPostgresConn::displayStringForGeomType( mSpatialColType ) ), 2 );
4413

    
4414
  return mValid;
4415
}
4416

    
4417
bool QgsPostgresProvider::convertField( QgsField &field, const QMap<QString, QVariant> *options )
4418
{
4419
  //determine field type to use for strings
4420
  QString stringFieldType = QStringLiteral( "varchar" );
4421
  if ( options && options->value( QStringLiteral( "dropStringConstraints" ), false ).toBool() )
4422
  {
4423
    //drop string length constraints by using PostgreSQL text type for strings
4424
    stringFieldType = QStringLiteral( "text" );
4425
  }
4426

    
4427
  QString fieldType = stringFieldType; //default to string
4428
  int fieldSize = field.length();
4429
  int fieldPrec = field.precision();
4430
  switch ( field.type() )
4431
  {
4432
    case QMetaType::Type::LongLong:
4433
      fieldType = QStringLiteral( "int8" );
4434
      fieldPrec = 0;
4435
      break;
4436

    
4437
    case QMetaType::Type::QDateTime:
4438
      fieldType = QStringLiteral( "timestamp without time zone" );
4439
      break;
4440

    
4441
    case QMetaType::Type::QTime:
4442
      fieldType = QStringLiteral( "time" );
4443
      break;
4444

    
4445
    case QMetaType::Type::QString:
4446
      fieldType = stringFieldType;
4447
      fieldPrec = 0;
4448
      break;
4449

    
4450
    case QMetaType::Type::Int:
4451
      fieldType = QStringLiteral( "int4" );
4452
      fieldPrec = 0;
4453
      break;
4454

    
4455
    case QMetaType::Type::QDate:
4456
      fieldType = QStringLiteral( "date" );
4457
      fieldPrec = 0;
4458
      break;
4459

    
4460
    case QMetaType::Type::QVariantMap:
4461
      fieldType = field.typeName();
4462
      if ( fieldType.isEmpty() )
4463
        fieldType = QStringLiteral( "hstore" );
4464
      fieldPrec = 0;
4465
      break;
4466

    
4467
    case QMetaType::Type::QStringList:
4468
      fieldType = QStringLiteral( "_text" );
4469
      fieldPrec = 0;
4470
      break;
4471

    
4472
    case QMetaType::Type::QVariantList:
4473
    {
4474
      QgsField sub( QString(), field.subType(), QString(), fieldSize, fieldPrec );
4475
      if ( !convertField( sub, nullptr ) )
4476
        return false;
4477
      fieldType = "_" + sub.typeName();
4478
      fieldPrec = 0;
4479
      break;
4480
    }
4481

    
4482
    case QMetaType::Type::Double:
4483
      if ( fieldSize > 18 )
4484
      {
4485
        fieldType = QStringLiteral( "numeric" );
4486
        fieldSize = -1;
4487
      }
4488
      else
4489
      {
4490
        fieldType = QStringLiteral( "float8" );
4491
      }
4492
      fieldPrec = 0;
4493
      break;
4494

    
4495
    case QMetaType::Type::Bool:
4496
      fieldType = QStringLiteral( "bool" );
4497
      fieldPrec = 0;
4498
      fieldSize = -1;
4499
      break;
4500

    
4501
    case QMetaType::Type::QByteArray:
4502
      fieldType = QStringLiteral( "bytea" );
4503
      fieldPrec = 0;
4504
      break;
4505

    
4506
    default:
4507
      return false;
4508
  }
4509

    
4510
  field.setTypeName( fieldType );
4511
  field.setLength( fieldSize );
4512
  field.setPrecision( fieldPrec );
4513
  return true;
4514
}
4515

    
4516

    
4517
void postgisGeometryType( Qgis::WkbType wkbType, QString &geometryType, int &dim )
4518
{
4519
  dim = 2;
4520
  Qgis::WkbType flatType = QgsWkbTypes::flatType( wkbType );
4521
  geometryType = QgsWkbTypes::displayString( flatType ).toUpper();
4522
  switch ( flatType )
4523
  {
4524
    case Qgis::WkbType::Unknown:
4525
      geometryType = QStringLiteral( "GEOMETRY" );
4526
      break;
4527

    
4528
    case Qgis::WkbType::NoGeometry:
4529
      geometryType.clear();
4530
      dim = 0;
4531
      break;
4532

    
4533
    default:
4534
      break;
4535
  }
4536

    
4537
  if ( QgsWkbTypes::hasZ( wkbType ) && QgsWkbTypes::hasM( wkbType ) )
4538
  {
4539
    dim = 4;
4540
  }
4541
  else if ( QgsWkbTypes::hasZ( wkbType ) )
4542
  {
4543
    dim = 3;
4544
  }
4545
  else if ( QgsWkbTypes::hasM( wkbType ) )
4546
  {
4547
    geometryType += QLatin1Char( 'M' );
4548
    dim = 3;
4549
  }
4550
  else if ( wkbType >= Qgis::WkbType::Point25D && wkbType <= Qgis::WkbType::MultiPolygon25D )
4551
  {
4552
    dim = 3;
4553
  }
4554
}
4555

    
4556
Qgis::VectorExportResult QgsPostgresProvider::createEmptyLayer( const QString &uri, const QgsFields &fields, Qgis::WkbType wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap<int, int> *oldToNewAttrIdxMap, QString *errorMessage, const QMap<QString, QVariant> *options )
4557
{
4558
  // populate members from the uri structure
4559
  QgsDataSourceUri dsUri( uri );
4560

    
4561
  QString schemaName = dsUri.schema();
4562
  QString tableName = dsUri.table();
4563

    
4564
  QString geometryColumn = dsUri.geometryColumn();
4565
  QString geometryType;
4566

    
4567
  QString primaryKey = dsUri.keyColumn();
4568
  QString primaryKeyType;
4569

    
4570
  QStringList pkList;
4571
  QStringList pkType;
4572

    
4573
  QString schemaTableName;
4574
  if ( !schemaName.isEmpty() )
4575
  {
4576
    schemaTableName += quotedIdentifier( schemaName ) + '.';
4577
  }
4578
  schemaTableName += quotedIdentifier( tableName );
4579

    
4580
  QgsDebugMsgLevel( QStringLiteral( "Connection info is: %1" ).arg( dsUri.connectionInfo( false ) ), 2 );
4581
  QgsDebugMsgLevel( QStringLiteral( "Geometry column is: %1" ).arg( geometryColumn ), 2 );
4582
  QgsDebugMsgLevel( QStringLiteral( "Schema is: %1" ).arg( schemaName ), 2 );
4583
  QgsDebugMsgLevel( QStringLiteral( "Table name is: %1" ).arg( tableName ), 2 );
4584

    
4585
  // create the table
4586
  QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, false );
4587
  if ( !conn )
4588
  {
4589
    if ( errorMessage )
4590
      *errorMessage = QObject::tr( "Connection to database failed" );
4591
    return Qgis::VectorExportResult::ErrorConnectionFailed;
4592
  }
4593

    
4594
  // get the pk's name and type
4595
  // Try to find a PK candidate from numeric NOT NULL / UNIQUE columns
4596
  if ( primaryKey.isEmpty() )
4597
  {
4598
    for ( const auto &field : std::as_const( fields ) )
4599
    {
4600
      if ( field.isNumeric() && ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintUnique ) && ( field.constraints().constraints() & QgsFieldConstraints::Constraint::ConstraintNotNull ) && ( field.constraints().constraints() & QgsFieldConstraints::ConstraintOrigin::ConstraintOriginProvider ) )
4601
      {
4602
        primaryKey = field.name();
4603
        break;
4604
      }
4605
    }
4606
  }
4607

    
4608
  // if no pk name was passed or guessed, define the new pk field name
4609
  if ( primaryKey.isEmpty() )
4610
  {
4611
    int index = 0;
4612
    QString pk = primaryKey = QStringLiteral( "id" );
4613
    for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx )
4614
    {
4615
      if ( fields.at( fldIdx ).name() == primaryKey )
4616
      {
4617
        // it already exists, try again with a new name
4618
        primaryKey = QStringLiteral( "%1_%2" ).arg( pk ).arg( index++ );
4619
        fldIdx = -1; // it is incremented in the for loop, i.e. restarts at 0
4620
      }
4621
    }
4622

    
4623
    pkList = QStringList( primaryKey );
4624
    pkType = QStringList( QStringLiteral( "serial" ) );
4625
  }
4626
  else
4627
  {
4628
    pkList = parseUriKey( primaryKey );
4629
    const auto constPkList = pkList;
4630
    const bool lowercaseFieldNames = options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool();
4631
    for ( const QString &col : constPkList )
4632
    {
4633
      // search for the passed field
4634
      QString type;
4635
      for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx )
4636
      {
4637
        const QString fieldName = lowercaseFieldNames ? fields[fldIdx].name().toLower() : fields[fldIdx].name();
4638
        if ( fieldName == col )
4639
        {
4640
          // found, get the field type
4641
          QgsField fld = fields[fldIdx];
4642
          if ( convertField( fld, options ) )
4643
          {
4644
            type = fld.typeName();
4645
            break;
4646
          }
4647
        }
4648
      }
4649
      if ( type.isEmpty() )
4650
        type = QStringLiteral( "serial" );
4651
      else
4652
      {
4653
        // if the pk field's type is one of the postgres integer types,
4654
        // use the equivalent autoincremental type (serialN)
4655
        if ( primaryKeyType == QLatin1String( "int2" ) || primaryKeyType == QLatin1String( "int4" ) )
4656
        {
4657
          primaryKeyType = QStringLiteral( "serial" );
4658
        }
4659
        else if ( primaryKeyType == QLatin1String( "int8" ) )
4660
        {
4661
          primaryKeyType = QStringLiteral( "serial8" );
4662
        }
4663
      }
4664
      pkType << type;
4665
    }
4666
  }
4667

    
4668
  try
4669
  {
4670
    conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "BEGIN" ) );
4671

    
4672
    // We want a valid schema name ...
4673
    if ( schemaName.isEmpty() )
4674
    {
4675
      QString sql = QString( "SELECT current_schema" );
4676
      QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
4677
      if ( result.PQresultStatus() != PGRES_TUPLES_OK )
4678
        throw PGException( result );
4679
      schemaName = result.PQgetvalue( 0, 0 );
4680
      if ( schemaName.isEmpty() )
4681
      {
4682
        schemaName = QStringLiteral( "public" );
4683
      }
4684
    }
4685

    
4686
    QString sql = QString( "SELECT 1"
4687
                           " FROM pg_class AS cls JOIN pg_namespace AS nsp"
4688
                           " ON nsp.oid=cls.relnamespace "
4689
                           " WHERE cls.relname=%1 AND nsp.nspname=%2" )
4690
                    .arg( quotedValue( tableName ), quotedValue( schemaName ) );
4691

    
4692
    QgsPostgresResult result( conn->LoggedPQexec( "QgsPostgresProvider", sql ) );
4693
    if ( result.PQresultStatus() != PGRES_TUPLES_OK )
4694
      throw PGException( result );
4695

    
4696
    bool exists = result.PQntuples() > 0;
4697

    
4698
    if ( exists && overwrite )
4699
    {
4700
      // delete the table if exists, then re-create it
4701
      QString sql = QString( "SELECT DropGeometryTable(%1,%2)"
4702
                             " FROM pg_class AS cls JOIN pg_namespace AS nsp"
4703
                             " ON nsp.oid=cls.relnamespace "
4704
                             " WHERE cls.relname=%2 AND nsp.nspname=%1" )
4705
                      .arg( quotedValue( schemaName ), quotedValue( tableName ) );
4706

    
4707
      result = conn->LoggedPQexec( "QgsPostgresProvider", sql );
4708
      if ( result.PQresultStatus() != PGRES_TUPLES_OK )
4709
        throw PGException( result );
4710
    }
4711

    
4712
    sql = QStringLiteral( "CREATE TABLE %1(" ).arg( schemaTableName );
4713
    QString pk;
4714
    for ( int i = 0; i < pkList.size(); ++i )
4715
    {
4716
      QString col = pkList[i];
4717
      const QString &type = pkType[i];
4718

    
4719
      if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() )
4720
      {
4721
        col = col.toLower();
4722
      }
4723
      else
4724
      {
4725
        col = quotedIdentifier( col ); // no need to quote lowercase field
4726
      }
4727

    
4728
      if ( i )
4729
      {
4730
        pk += QLatin1Char( ',' );
4731
        sql += QLatin1Char( ',' );
4732
      }
4733

    
4734
      pk += col;
4735
      sql += col + " " + type;
4736
    }
4737
    sql += QStringLiteral( ", PRIMARY KEY (%1) )" ).arg( pk );
4738

    
4739
    result = conn->LoggedPQexec( "QgsPostgresProvider", sql );
4740
    if ( result.PQresultStatus() != PGRES_COMMAND_OK )
4741
      throw PGException( result );
4742

    
4743
    // get geometry type, dim and srid
4744
    int dim = 2;
4745
    long srid = srs.postgisSrid();
4746

    
4747
    postgisGeometryType( wkbType, geometryType, dim );
4748

    
4749
    // create geometry column
4750
    if ( !geometryType.isEmpty() )
4751
    {
4752
      sql = QStringLiteral( "SELECT AddGeometryColumn(%1,%2,%3,%4,%5,%6)" )
4753
              .arg( quotedValue( schemaName ), quotedValue( tableName ), quotedValue( geometryColumn ) )
4754
              .arg( srid )
4755
              .arg( quotedValue( geometryType ) )
4756
              .arg( dim );
4757

    
4758
      result = conn->LoggedPQexec( "QgsPostgresProvider", sql );
4759
      if ( result.PQresultStatus() != PGRES_TUPLES_OK )
4760
        throw PGException( result );
4761
    }
4762
    else
4763
    {
4764
      geometryColumn.clear();
4765
    }
4766

    
4767
    conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "COMMIT" ) );
4768
  }
4769
  catch ( PGException &e )
4770
  {
4771
    if ( errorMessage )
4772
      *errorMessage = QObject::tr( "Creation of data source %1 failed: \n%2" )
4773
                        .arg( schemaTableName, e.errorMessage() );
4774

    
4775
    conn->LoggedPQexecNR( "QgsPostgresProvider", QStringLiteral( "ROLLBACK" ) );
4776
    conn->unref();
4777
    return Qgis::VectorExportResult::ErrorCreatingLayer;
4778
  }
4779
  conn->unref();
4780

    
4781
  QgsDebugMsgLevel( QStringLiteral( "layer %1 created" ).arg( schemaTableName ), 2 );
4782

    
4783
  // use the provider to edit the table
4784
  dsUri.setDataSource( schemaName, tableName, geometryColumn, QString(), primaryKey );
4785

    
4786
  QgsDataProvider::ProviderOptions providerOptions;
4787
  Qgis::DataProviderReadFlags flags;
4788
  std::unique_ptr<QgsPostgresProvider> provider = std::make_unique<QgsPostgresProvider>( dsUri.uri( false ), providerOptions, flags );
4789
  if ( !provider->isValid() )
4790
  {
4791
    if ( errorMessage )
4792
      *errorMessage = QObject::tr( "Loading of the layer %1 failed" ).arg( schemaTableName );
4793

    
4794
    return Qgis::VectorExportResult::ErrorInvalidLayer;
4795
  }
4796

    
4797
  QgsDebugMsgLevel( QStringLiteral( "layer loaded" ), 2 );
4798

    
4799
  // add fields to the layer
4800
  if ( oldToNewAttrIdxMap )
4801
    oldToNewAttrIdxMap->clear();
4802

    
4803
  if ( fields.size() > 0 )
4804
  {
4805
    int offset = 1;
4806

    
4807
    // get the list of fields
4808
    QList<QgsField> flist;
4809
    for ( int fldIdx = 0; fldIdx < fields.count(); ++fldIdx )
4810
    {
4811
      QgsField fld = fields.at( fldIdx );
4812

    
4813
      if ( fld.name() == geometryColumn )
4814
      {
4815
        //the "lowercaseFieldNames" option does not affect the name of the geometry column, so we perform
4816
        //this test before converting the field name to lowercase
4817
        QgsDebugMsgLevel( QStringLiteral( "Found a field with the same name of the geometry column. Skip it!" ), 2 );
4818
        continue;
4819
      }
4820

    
4821
      if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() )
4822
      {
4823
        //convert field name to lowercase
4824
        fld.setName( fld.name().toLower() );
4825
      }
4826

    
4827
      int pkIdx = -1;
4828
      for ( int i = 0; i < pkList.size(); ++i )
4829
      {
4830
        QString col = pkList[i];
4831
        if ( options && options->value( QStringLiteral( "lowercaseFieldNames" ), false ).toBool() )
4832
        {
4833
          //convert field name to lowercase (TODO: avoid doing this
4834
          //over and over)
4835
          col = col.toLower();
4836
        }
4837
        if ( fld.name() == col )
4838
        {
4839
          pkIdx = i;
4840
          break;
4841
        }
4842
      }
4843
      if ( pkIdx >= 0 )
4844
      {
4845
        oldToNewAttrIdxMap->insert( fldIdx, pkIdx );
4846
        continue;
4847
      }
4848

    
4849
      if ( !( options && options->value( QStringLiteral( "skipConvertFields" ), false ).toBool() ) && !convertField( fld, options ) )
4850
      {
4851
        if ( errorMessage )
4852
          *errorMessage = QObject::tr( "Unsupported type for field %1" ).arg( fld.name() );
4853

    
4854
        return Qgis::VectorExportResult::ErrorAttributeTypeUnsupported;
4855
      }
4856

    
4857
      QgsDebugMsgLevel( QStringLiteral( "creating field #%1 -> #%2 name %3 type %4 typename %5 width %6 precision %7" ).arg( fldIdx ).arg( offset ).arg( fld.name(), QVariant::typeToName( fld.type() ), fld.typeName() ).arg( fld.length() ).arg( fld.precision() ), 2 );
4858

    
4859
      flist.append( fld );
4860
      if ( oldToNewAttrIdxMap )
4861
        oldToNewAttrIdxMap->insert( fldIdx, offset++ );
4862
    }
4863

    
4864
    if ( !provider->addAttributes( flist ) )
4865
    {
4866
      if ( errorMessage )
4867
        *errorMessage = QObject::tr( "Creation of fields failed:\n%1" ).arg( provider->errors().join( '\n' ) );
4868

    
4869
      return Qgis::VectorExportResult::ErrorAttributeCreationFailed;
4870
    }
4871

    
4872
    QgsDebugMsgLevel( QStringLiteral( "Done creating fields" ), 2 );
4873
  }
4874
  return Qgis::VectorExportResult::Success;
4875
}
4876

    
4877
QgsCoordinateReferenceSystem QgsPostgresProvider::crs() const
4878
{
4879
  QgsCoordinateReferenceSystem srs;
4880
  int srid = mRequestedSrid.isEmpty() ? mDetectedSrid.toInt() : mRequestedSrid.toInt();
4881

    
4882
  return sridToCrs( srid, connectionRO() );
4883
}
4884

    
4885
QString QgsPostgresProvider::subsetString() const
4886
{
4887
  return mSqlWhereClause;
4888
}
4889

    
4890
QString QgsPostgresProvider::getTableName()
4891
{
4892
  return mTableName;
4893
}
4894

    
4895
size_t QgsPostgresProvider::layerCount() const
4896
{
4897
  return 1; // XXX need to return actual number of layers
4898
} // QgsPostgresProvider::layerCount()
4899

    
4900

    
4901
QString QgsPostgresProvider::name() const
4902
{
4903
  return POSTGRES_KEY;
4904
} //  QgsPostgresProvider::name()
4905

    
4906
QString QgsPostgresProvider::description() const
4907
{
4908
  QString pgVersion( tr( "PostgreSQL version: unknown" ) );
4909
  QString postgisVersion( tr( "unknown" ) );
4910

    
4911
  if ( auto *lConnectionRO = connectionRO() )
4912
  {
4913
    QgsPostgresResult result;
4914

    
4915
    /* TODO: expose a cached QgsPostgresConn::version() ? */
4916
    result = lConnectionRO->LoggedPQexec( QStringLiteral( "QgsPostgresProvider" ), QStringLiteral( "SELECT version()" ) );
4917
    if ( result.PQresultStatus() == PGRES_TUPLES_OK )
4918
    {
4919
      pgVersion = result.PQgetvalue( 0, 0 );
4920
    }
4921

    
4922
    postgisVersion = lConnectionRO->postgisVersion();
4923
  }
4924
  else
4925
  {
4926
    pgVersion = tr( "PostgreSQL not connected" );
4927
  }
4928

    
4929
  return tr( "PostgreSQL/PostGIS provider\n%1\nPostGIS %2" ).arg( pgVersion, postgisVersion );
4930
} //  QgsPostgresProvider::description()
4931

    
4932
static void jumpSpace( const QString &txt, int &i )
4933
{
4934
  while ( i < txt.length() && txt.at( i ).isSpace() )
4935
    ++i;
4936
}
4937

    
4938
QString QgsPostgresProvider::getNextString( const QString &txt, int &i, const QString &sep )
4939
{
4940
  jumpSpace( txt, i );
4941
  if ( i < txt.length() && txt.at( i ) == '"' )
4942
  {
4943
    const thread_local QRegularExpression stringRe( QRegularExpression::anchoredPattern( "^\"((?:\\\\.|[^\"\\\\])*)\".*" ) );
4944
    const QRegularExpressionMatch match = stringRe.match( txt.mid( i ) );
4945
    if ( !match.hasMatch() )
4946
    {
4947
      QgsMessageLog::logMessage( tr( "Cannot find end of double quoted string: %1" ).arg( txt ), tr( "PostGIS" ) );
4948
      return QString();
4949
    }
4950
    i += match.captured( 1 ).length() + 2;
4951
    jumpSpace( txt, i );
4952
    if ( !QStringView { txt }.mid( i ).startsWith( sep ) && i < txt.length() )
4953
    {
4954
      QgsMessageLog::logMessage( tr( "Cannot find separator: %1" ).arg( txt.mid( i ) ), tr( "PostGIS" ) );
4955
      return QString();
4956
    }
4957
    i += sep.length();
4958
    return match.captured( 1 ).replace( QLatin1String( "\\\"" ), QLatin1String( "\"" ) ).replace( QLatin1String( "\\\\" ), QLatin1String( "\\" ) );
4959
  }
4960
  else
4961
  {
4962
    int start = i;
4963
    for ( ; i < txt.length(); i++ )
4964
    {
4965
      if ( QStringView { txt }.mid( i ).startsWith( sep ) )
4966
      {
4967
        QStringView v( QStringView { txt }.mid( start, i - start ) );
4968
        i += sep.length();
4969
        return v.trimmed().toString();
4970
      }
4971
    }
4972
    return QStringView { txt }.mid( start, i - start ).trimmed().toString();
4973
  }
4974
}
4975

    
4976
QVariant QgsPostgresProvider::parseHstore( const QString &txt )
4977
{
4978
  QVariantMap result;
4979
  int i = 0;
4980
  while ( i < txt.length() )
4981
  {
4982
    QString key = getNextString( txt, i, QStringLiteral( "=>" ) );
4983
    QString value = getNextString( txt, i, QStringLiteral( "," ) );
4984
    if ( key.isNull() || value.isNull() )
4985
    {
4986
      QgsMessageLog::logMessage( tr( "Error parsing hstore: %1" ).arg( txt ), tr( "PostGIS" ) );
4987
      break;
4988
    }
4989
    result.insert( key, value );
4990
  }
4991

    
4992
  return result;
4993
}
4994

    
4995
QVariant QgsPostgresProvider::parseJson( const QString &txt )
4996
{
4997
  return QgsJsonUtils::parseJson( txt );
4998
}
4999

    
5000
QVariant QgsPostgresProvider::parseOtherArray( const QString &txt, QMetaType::Type subType, const QString &typeName, QgsPostgresConn *conn )
5001
{
5002
  int i = 0;
5003
  QVariantList result;
5004
  while ( i < txt.length() )
5005
  {
5006
    const QString value = getNextString( txt, i, QStringLiteral( "," ) );
5007
    if ( value.isNull() )
5008
    {
5009
      QgsMessageLog::logMessage( tr( "Error parsing array: %1" ).arg( txt ), tr( "PostGIS" ) );
5010
      break;
5011
    }
5012
    result.append( convertValue( subType, QMetaType::Type::UnknownType, value, typeName, conn ) );
5013
  }
5014
  return result;
5015
}
5016

    
5017
QVariant QgsPostgresProvider::parseStringArray( const QString &txt )
5018
{
5019
  int i = 0;
5020
  QStringList result;
5021
  while ( i < txt.length() )
5022
  {
5023
    const QString value = getNextString( txt, i, QStringLiteral( "," ) );
5024
    if ( value.isNull() )
5025
    {
5026
      QgsMessageLog::logMessage( tr( "Error parsing array: %1" ).arg( txt ), tr( "PostGIS" ) );
5027
      break;
5028
    }
5029
    result.append( value );
5030
  }
5031
  return result;
5032
}
5033

    
5034
QVariant QgsPostgresProvider::parseMultidimensionalArray( const QString &txt )
5035
{
5036
  QStringList result;
5037
  if ( !txt.startsWith( '{' ) || !txt.endsWith( '}' ) )
5038
  {
5039
    QgsMessageLog::logMessage( tr( "Error parsing array, missing curly braces: %1" ).arg( txt ), tr( "PostGIS" ) );
5040
    return result;
5041
  }
5042

    
5043
  QStringList values;
5044
  QString text = txt;
5045
  while ( !text.isEmpty() )
5046
  {
5047
    bool escaped = false;
5048
    int openedBrackets = 1;
5049
    int i = 0;
5050
    while ( i < text.length() && openedBrackets > 0 )
5051
    {
5052
      ++i;
5053

    
5054
      if ( text.at( i ) == '}' && !escaped )
5055
        openedBrackets--;
5056
      else if ( text.at( i ) == '{' && !escaped )
5057
        openedBrackets++;
5058

    
5059
      escaped = !escaped ? text.at( i ) == '\\' : false;
5060
    }
5061

    
5062
    values.append( text.left( ++i ) );
5063
    i = text.indexOf( ',', i );
5064
    i = i > 0 ? text.indexOf( '{', i ) : -1;
5065
    if ( i == -1 )
5066
      break;
5067

    
5068
    text = text.mid( i );
5069
  }
5070
  return values;
5071
}
5072

    
5073
QVariant QgsPostgresProvider::parseArray( const QString &txt, QMetaType::Type type, QMetaType::Type subType, const QString &typeName, QgsPostgresConn *conn )
5074
{
5075
  if ( !txt.startsWith( '{' ) || !txt.endsWith( '}' ) )
5076
  {
5077
    if ( !txt.isEmpty() )
5078
      QgsMessageLog::logMessage( tr( "Error parsing array, missing curly braces: %1" ).arg( txt ), tr( "PostGIS" ) );
5079
    return QgsVariantUtils::createNullVariant( type );
5080
  }
5081
  QString inner = txt.mid( 1, txt.length() - 2 );
5082
  if ( ( type == QMetaType::Type::QStringList || type == QMetaType::Type::QVariantList ) && inner.startsWith( "{" ) )
5083
    return parseMultidimensionalArray( inner );
5084
  else if ( type == QMetaType::Type::QStringList )
5085
    return parseStringArray( inner );
5086
  else
5087
    return parseOtherArray( inner, subType, typeName, conn );
5088
}
5089

    
5090
QVariant QgsPostgresProvider::convertValue( QMetaType::Type type, QMetaType::Type subType, const QString &value, const QString &typeName ) const
5091
{
5092
  return convertValue( type, subType, value, typeName, connectionRO() );
5093
}
5094

    
5095
QVariant QgsPostgresProvider::convertValue( QMetaType::Type type, QMetaType::Type subType, const QString &value, const QString &typeName, QgsPostgresConn *conn )
5096
{
5097
  QVariant result;
5098
  switch ( type )
5099
  {
5100
    case QMetaType::Type::QVariantMap:
5101
      if ( typeName == QLatin1String( "json" ) || typeName == QLatin1String( "jsonb" ) )
5102
        result = parseJson( value );
5103
      else
5104
        result = parseHstore( value );
5105
      break;
5106
    case QMetaType::Type::QStringList:
5107
    case QMetaType::Type::QVariantList:
5108
      result = parseArray( value, type, subType, typeName, conn );
5109
      break;
5110
    case QMetaType::Type::Bool:
5111
      if ( value == QChar( 't' ) )
5112
        result = true;
5113
      else if ( value == QChar( 'f' ) )
5114
        result = false;
5115
      else
5116
        result = QgsVariantUtils::createNullVariant( type );
5117
      break;
5118
    case QMetaType::Type::User:
5119
      result = fromEwkt( value, conn );
5120
      break;
5121

    
5122
    default:
5123
      result = value;
5124
      if ( !result.convert( type ) || value.isNull() )
5125
        result = QgsVariantUtils::createNullVariant( type );
5126
      break;
5127
  }
5128

    
5129
  return result;
5130
}
5131

    
5132
QList<QgsVectorLayer *> QgsPostgresProvider::searchLayers( const QList<QgsVectorLayer *> &layers, const QString &connectionInfo, const QString &schema, const QString &tableName )
5133
{
5134
  QList<QgsVectorLayer *> result;
5135
  const auto constLayers = layers;
5136
  for ( QgsVectorLayer *layer : constLayers )
5137
  {
5138
    const QgsPostgresProvider *pgProvider = qobject_cast<QgsPostgresProvider *>( layer->dataProvider() );
5139
    if ( pgProvider && pgProvider->mUri.connectionInfo( false ) == connectionInfo && pgProvider->mSchemaName == schema && pgProvider->mTableName == tableName )
5140
    {
5141
      result.append( layer );
5142
    }
5143
  }
5144
  return result;
5145
}
5146

    
5147
QList<QgsRelation> QgsPostgresProvider::discoverRelations( const QgsVectorLayer *target, const QList<QgsVectorLayer *> &layers ) const
5148
{
5149
  QList<QgsRelation> result;
5150

    
5151
  // Silently skip if this is a query layer or for some obscure reason there are no table and schema name
5152
  if ( mIsQuery || mTableName.isEmpty() || mSchemaName.isEmpty() )
5153
  {
5154
    return result;
5155
  }
5156

    
5157
  // Skip less silently if layer is not valid
5158
  if ( !mValid )
5159
  {
5160
    QgsLogger::warning( tr( "Error discovering relations of %1: invalid layer" ).arg( mQuery ) );
5161
    return result;
5162
  }
5163

    
5164

    
5165
  QString sql(
5166
    "WITH foreign_keys AS "
5167
    "  ( SELECT c.conname, "
5168
    "           c.conrelid, "
5169
    "           c.confrelid, "
5170
    "           unnest(c.conkey) AS conkey, "
5171
    "           unnest(c.confkey) AS confkey, "
5172
    "     (SELECT relname "
5173
    "      FROM pg_catalog.pg_class "
5174
    "      WHERE oid = c.conrelid) as referencing_table, "
5175
    "     (SELECT relname "
5176
    "      FROM pg_catalog.pg_class "
5177
    "      WHERE oid = c.confrelid) as referenced_table, "
5178
    "     (SELECT relnamespace::regnamespace::text "
5179
    "      FROM pg_catalog.pg_class "
5180
    "      WHERE oid = c.confrelid) as constraint_schema "
5181
    "   FROM pg_constraint c "
5182
    "   WHERE contype = 'f' "
5183
    "     AND c.conrelid::regclass = "
5184
    + QgsPostgresConn::quotedValue( QString( QgsPostgresConn::quotedIdentifier( mSchemaName ) + '.' + QgsPostgresConn::quotedIdentifier( mTableName ) ) ) + "::regclass ) "
5185
                                                                                                                                                            "SELECT fk.conname as constraint_name, "
5186
                                                                                                                                                            "       a.attname as column_name, "
5187
                                                                                                                                                            "       fk.constraint_schema, "
5188
                                                                                                                                                            "       referenced_table as table_name, "
5189
                                                                                                                                                            "       af.attname as column_name "
5190
                                                                                                                                                            "FROM foreign_keys fk "
5191
                                                                                                                                                            "JOIN pg_attribute af ON af.attnum = fk.confkey "
5192
                                                                                                                                                            "AND af.attrelid = fk.confrelid "
5193
                                                                                                                                                            "JOIN pg_attribute a ON a.attnum = conkey "
5194
                                                                                                                                                            "AND a.attrelid = fk.conrelid "
5195
                                                                                                                                                            "ORDER BY fk.confrelid, "
5196
                                                                                                                                                            "         fk.conname ;"
5197
  );
5198

    
5199
  QgsPostgresResult sqlResult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
5200
  if ( sqlResult.PQresultStatus() != PGRES_TUPLES_OK )
5201
  {
5202
    QgsLogger::warning( "Error getting the foreign keys of " + mTableName );
5203
    return result;
5204
  }
5205

    
5206
  QList<QString> refTableFound;
5207
  for ( int row = 0; row < sqlResult.PQntuples(); ++row )
5208
  {
5209
    const QString name = sqlResult.PQgetvalue( row, 0 );
5210
    const QString fkColumn = sqlResult.PQgetvalue( row, 1 );
5211
    QString refSchema = sqlResult.PQgetvalue( row, 2 );
5212
    QString refTable = sqlResult.PQgetvalue( row, 3 );
5213
    // Strip quotes
5214
    if ( refTable.startsWith( '"' ) && refTable.endsWith( '"' ) )
5215
    {
5216
      refTable = refTable.mid( 1, refTable.length() - 2 );
5217
    }
5218
    if ( refSchema.startsWith( '"' ) && refSchema.endsWith( '"' ) )
5219
    {
5220
      refSchema = refSchema.mid( 1, refSchema.length() - 2 );
5221
    }
5222
    const QString refColumn = sqlResult.PQgetvalue( row, 4 );
5223
    // try to find if we have layers for the referenced table
5224
    const QList<QgsVectorLayer *> foundLayers = searchLayers( layers, mUri.connectionInfo( false ), refSchema, refTable );
5225
    if ( !refTableFound.contains( refTable ) )
5226
    {
5227
      for ( const QgsVectorLayer *foundLayer : foundLayers )
5228
      {
5229
        QgsRelation relation;
5230
        relation.setName( name );
5231
        relation.setReferencingLayer( target->id() );
5232
        relation.setReferencedLayer( foundLayer->id() );
5233
        relation.addFieldPair( fkColumn, refColumn );
5234
        relation.generateId();
5235
        if ( relation.isValid() )
5236
        {
5237
          result.append( relation );
5238
          refTableFound.append( refTable );
5239
        }
5240
        else
5241
        {
5242
          QgsLogger::warning( "Invalid relation for " + name );
5243
        }
5244
      }
5245
    }
5246
    else
5247
    {
5248
      // multi reference field => add the field pair to all the referenced layers found
5249
      const int resultSize = result.size();
5250
      for ( int i = 0; i < resultSize; ++i )
5251
      {
5252
        for ( const QgsVectorLayer *foundLayer : foundLayers )
5253
        {
5254
          if ( result[resultSize - 1 - i].referencedLayerId() == foundLayer->id() )
5255
          {
5256
            result[resultSize - 1 - i].addFieldPair( fkColumn, refColumn );
5257
          }
5258
        }
5259
      }
5260
    }
5261
  }
5262
  return result;
5263
}
5264

    
5265
void QgsPostgresProvider::setQuery( const QString &query )
5266
{
5267
  mQuery = query;
5268

    
5269
  mKind = Qgis::PostgresRelKind::NotSet;
5270
}
5271

    
5272
Qgis::PostgresRelKind QgsPostgresProvider::relkind() const
5273
{
5274
  if ( mKind != Qgis::PostgresRelKind::NotSet )
5275
    return mKind;
5276

    
5277
  if ( mIsQuery || !connectionRO() )
5278
  {
5279
    mKind = Qgis::PostgresRelKind::Unknown;
5280
  }
5281
  else
5282
  {
5283
    QString sql = QStringLiteral( "SELECT relkind FROM pg_class WHERE oid=regclass(%1)::oid" ).arg( quotedValue( mQuery ) );
5284
    QgsPostgresResult res( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) );
5285
    QString type = res.PQgetvalue( 0, 0 );
5286

    
5287
    mKind = QgsPostgresConn::relKindFromValue( type );
5288
  }
5289

    
5290
  return mKind;
5291
}
5292

    
5293
bool QgsPostgresProvider::hasMetadata() const
5294
{
5295
  bool hasMetadata = true;
5296
  Qgis::PostgresRelKind kind = relkind();
5297

    
5298
  if ( kind == Qgis::PostgresRelKind::View || kind == Qgis::PostgresRelKind::MaterializedView )
5299
  {
5300
    hasMetadata = false;
5301
  }
5302

    
5303
  return hasMetadata;
5304
}
5305

    
5306
QgsDataProvider *QgsPostgresProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, Qgis::DataProviderReadFlags flags )
5307
{
5308
  return new QgsPostgresProvider( uri, options, flags );
5309
}
5310

    
5311
QList<QgsDataItemProvider *> QgsPostgresProviderMetadata::dataItemProviders() const
5312
{
5313
  QList<QgsDataItemProvider *> providers;
5314
  providers << new QgsPostgresDataItemProvider;
5315
  return providers;
5316
}
5317

    
5318
// ---------------------------------------------------------------------------
5319

    
5320
Qgis::VectorExportResult QgsPostgresProviderMetadata::createEmptyLayer( const QString &uri, const QgsFields &fields, Qgis::WkbType wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap<int, int> &oldToNewAttrIdxMap, QString &errorMessage, const QMap<QString, QVariant> *options )
5321
{
5322
  return QgsPostgresProvider::createEmptyLayer(
5323
    uri, fields, wkbType, srs, overwrite,
5324
    &oldToNewAttrIdxMap, &errorMessage, options
5325
  );
5326
}
5327

    
5328
bool QgsPostgresProviderMetadata::styleExists( const QString &uri, const QString &styleId, QString &errorCause )
5329
{
5330
  errorCause.clear();
5331

    
5332
  QgsDataSourceUri dsUri( uri );
5333
  QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, true );
5334
  if ( !conn )
5335
  {
5336
    errorCause = QObject::tr( "Connection to database failed" );
5337
    return false;
5338
  }
5339

    
5340
  if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) )
5341
  {
5342
    return false;
5343
  }
5344
  else if ( !columnExists( *conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) )
5345
  {
5346
    return false;
5347
  }
5348

    
5349
  if ( dsUri.database().isEmpty() ) // typically when a service file is used
5350
  {
5351
    dsUri.setDatabase( conn->currentDatabase() );
5352
  }
5353

    
5354
  const QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
5355

    
5356
  const QString checkQuery = QString( "SELECT styleName"
5357
                                      " FROM layer_styles"
5358
                                      " WHERE f_table_catalog=%1"
5359
                                      " AND f_table_schema=%2"
5360
                                      " AND f_table_name=%3"
5361
                                      " AND f_geometry_column %4"
5362
                                      " AND (type=%5 OR type IS NULL)"
5363
                                      " AND styleName=%6" )
5364
                               .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5365
                               .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5366
                               .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5367
                               .arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "= %1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
5368
                               .arg( wkbTypeString )
5369
                               .arg( QgsPostgresConn::quotedValue( styleId.isEmpty() ? dsUri.table() : styleId ) );
5370

    
5371
  QgsPostgresResult res( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), checkQuery ) );
5372
  if ( res.PQresultStatus() == PGRES_TUPLES_OK )
5373
  {
5374
    return res.PQntuples() > 0;
5375
  }
5376
  else
5377
  {
5378
    errorCause = res.PQresultErrorMessage();
5379
    return false;
5380
  }
5381
}
5382

    
5383
bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString &qmlStyleIn, const QString &sldStyleIn, const QString &styleName, const QString &styleDescription, const QString &uiFileContent, bool useAsDefault, QString &errCause )
5384
{
5385
  QgsDataSourceUri dsUri( uri );
5386

    
5387
  // Replace invalid XML characters
5388
  QString qmlStyle { qmlStyleIn };
5389
  QgsPostgresUtils::replaceInvalidXmlChars( qmlStyle );
5390
  QString sldStyle { sldStyleIn };
5391
  QgsPostgresUtils::replaceInvalidXmlChars( sldStyle );
5392

    
5393
  QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, false );
5394
  if ( !conn )
5395
  {
5396
    errCause = QObject::tr( "Connection to database failed" );
5397
    return false;
5398
  }
5399

    
5400
  if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) )
5401
  {
5402
    QgsPostgresResult res( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), "CREATE TABLE layer_styles("
5403
                                                                                                "id SERIAL PRIMARY KEY"
5404
                                                                                                ",f_table_catalog varchar"
5405
                                                                                                ",f_table_schema varchar"
5406
                                                                                                ",f_table_name varchar"
5407
                                                                                                ",f_geometry_column varchar"
5408
                                                                                                ",styleName text"
5409
                                                                                                ",styleQML xml"
5410
                                                                                                ",styleSLD xml"
5411
                                                                                                ",useAsDefault boolean"
5412
                                                                                                ",description text"
5413
                                                                                                ",owner varchar(63) DEFAULT CURRENT_USER"
5414
                                                                                                ",ui xml"
5415
                                                                                                ",update_time timestamp DEFAULT CURRENT_TIMESTAMP"
5416
                                                                                                ",type varchar"
5417
                                                                                                ")" ) );
5418
    if ( res.PQresultStatus() != PGRES_COMMAND_OK )
5419
    {
5420
      errCause = QObject::tr( "Unable to save layer style. It's not possible to create the destination table on the database. Maybe this is due to table permissions (user=%1). Please contact your database admin" ).arg( dsUri.username() );
5421
      conn->unref();
5422
      return false;
5423
    }
5424
  }
5425
  else
5426
  {
5427
    if ( !columnExists( *conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) )
5428
    {
5429
      QgsPostgresResult res( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), "ALTER TABLE layer_styles ADD COLUMN type varchar NULL" ) );
5430
      if ( res.PQresultStatus() != PGRES_COMMAND_OK )
5431
      {
5432
        errCause = QObject::tr( "Unable to add column type to layer_styles table. Maybe this is due to table permissions (user=%1). Please contact your database admin" ).arg( dsUri.username() );
5433
        conn->unref();
5434
        return false;
5435
      }
5436
    }
5437
  }
5438

    
5439
  if ( dsUri.database().isEmpty() ) // typically when a service file is used
5440
  {
5441
    dsUri.setDatabase( conn->currentDatabase() );
5442
  }
5443

    
5444
  QString uiFileColumn;
5445
  QString uiFileValue;
5446
  if ( !uiFileContent.isEmpty() )
5447
  {
5448
    uiFileColumn = QStringLiteral( ",ui" );
5449
    uiFileValue = QStringLiteral( ",XMLPARSE(DOCUMENT %1)" ).arg( QgsPostgresConn::quotedValue( uiFileContent ) );
5450
  }
5451

    
5452
  const QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
5453

    
5454
  // Note: in the construction of the INSERT and UPDATE strings the qmlStyle and sldStyle values
5455
  // can contain user entered strings, which may themselves include %## values that would be
5456
  // replaced by the QString.arg function.  To ensure that the final SQL string is not corrupt these
5457
  // two values are both replaced in the final .arg call of the string construction.
5458

    
5459
  QString sql = QString( "INSERT INTO layer_styles("
5460
                         "f_table_catalog,f_table_schema,f_table_name,f_geometry_column,styleName,styleQML,styleSLD,useAsDefault,description,owner,type%12"
5461
                         ") VALUES ("
5462
                         "%1,%2,%3,%4,%5,XMLPARSE(DOCUMENT %16),XMLPARSE(DOCUMENT %17),%8,%9,%10,%11%13"
5463
                         ")" )
5464
                  .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5465
                  .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5466
                  .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5467
                  .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) )
5468
                  .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) )
5469
                  .arg( useAsDefault ? "true" : "false" )
5470
                  .arg( QgsPostgresConn::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
5471
                  .arg( "CURRENT_USER" )
5472
                  .arg( uiFileColumn )
5473
                  .arg( uiFileValue )
5474
                  .arg( wkbTypeString )
5475
                  // Must be the final .arg replacement - see above
5476
                  .arg( QgsPostgresConn::quotedValue( qmlStyle ), QgsPostgresConn::quotedValue( sldStyle ) );
5477

    
5478
  QString checkQuery = QString( "SELECT styleName"
5479
                                " FROM layer_styles"
5480
                                " WHERE f_table_catalog=%1"
5481
                                " AND f_table_schema=%2"
5482
                                " AND f_table_name=%3"
5483
                                " AND f_geometry_column %4"
5484
                                " AND (type=%5 OR type IS NULL)"
5485
                                " AND styleName=%6" )
5486
                         .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5487
                         .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5488
                         .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5489
                         .arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
5490
                         .arg( wkbTypeString )
5491
                         .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
5492

    
5493
  QgsPostgresResult res( conn->LoggedPQexec( "QgsPostgresProviderMetadata", checkQuery ) );
5494
  if ( res.PQntuples() > 0 )
5495
  {
5496
    sql = QString( "UPDATE layer_styles"
5497
                   " SET useAsDefault=%1"
5498
                   ",styleQML=XMLPARSE(DOCUMENT %12)"
5499
                   ",styleSLD=XMLPARSE(DOCUMENT %13)"
5500
                   ",description=%4"
5501
                   ",owner=%5"
5502
                   ",type=%2"
5503
                   " WHERE f_table_catalog=%6"
5504
                   " AND f_table_schema=%7"
5505
                   " AND f_table_name=%8"
5506
                   " AND f_geometry_column %9"
5507
                   " AND styleName=%10"
5508
                   " AND (type=%2 OR type IS NULL)" )
5509
            .arg( useAsDefault ? "true" : "false" )
5510
            .arg( wkbTypeString )
5511
            .arg( QgsPostgresConn::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
5512
            .arg( "CURRENT_USER" )
5513
            .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5514
            .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5515
            .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5516
            .arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
5517
            .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) )
5518
            // Must be the final .arg replacement - see above
5519
            .arg( QgsPostgresConn::quotedValue( qmlStyle ), QgsPostgresConn::quotedValue( sldStyle ) );
5520
  }
5521

    
5522
  if ( useAsDefault )
5523
  {
5524
    QString removeDefaultSql = QString( "UPDATE layer_styles"
5525
                                        " SET useAsDefault=false"
5526
                                        " WHERE f_table_catalog=%1"
5527
                                        " AND f_table_schema=%2"
5528
                                        " AND f_table_name=%3"
5529
                                        " AND f_geometry_column %4"
5530
                                        " AND (type=%5 OR type IS NULL)" )
5531
                                 .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5532
                                 .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5533
                                 .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5534
                                 .arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
5535
                                 .arg( wkbTypeString );
5536

    
5537
    sql = QStringLiteral( "BEGIN; %1; %2; COMMIT;" ).arg( removeDefaultSql, sql );
5538
  }
5539

    
5540
  res = conn->LoggedPQexec( "QgsPostgresProviderMetadata", sql );
5541

    
5542
  bool saved = res.PQresultStatus() == PGRES_COMMAND_OK;
5543
  if ( !saved )
5544
    errCause = QObject::tr( "Unable to save layer style. It's not possible to insert a new record into the style table. Maybe this is due to table permissions (user=%1). Please contact your database administrator." ).arg( dsUri.username() );
5545

    
5546
  conn->unref();
5547

    
5548
  return saved;
5549
}
5550

    
5551

    
5552
QString QgsPostgresProviderMetadata::loadStyle( const QString &uri, QString &errCause )
5553
{
5554
  QString styleName;
5555
  return loadStoredStyle( uri, styleName, errCause );
5556
}
5557

    
5558
QString QgsPostgresProviderMetadata::loadStoredStyle( const QString &uri, QString &styleName, QString &errCause )
5559
{
5560
  QgsDataSourceUri dsUri( uri );
5561
  QString selectQmlQuery;
5562

    
5563
  QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, true );
5564
  if ( !conn )
5565
  {
5566
    errCause = QObject::tr( "Connection to database failed" );
5567
    return QString();
5568
  }
5569

    
5570
  if ( dsUri.database().isEmpty() ) // typically when a service file is used
5571
  {
5572
    dsUri.setDatabase( conn->currentDatabase() );
5573
  }
5574

    
5575
  if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) )
5576
  {
5577
    conn->unref();
5578
    return QString();
5579
  }
5580

    
5581
  QString geomColumnExpr;
5582
  if ( dsUri.geometryColumn().isEmpty() )
5583
  {
5584
    geomColumnExpr = QStringLiteral( "IS NULL" );
5585
  }
5586
  else
5587
  {
5588
    geomColumnExpr = QStringLiteral( "=" ) + QgsPostgresConn::quotedValue( dsUri.geometryColumn() );
5589
  }
5590

    
5591
  QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
5592

    
5593
  // support layer_styles without type column < 3.14
5594
  if ( !columnExists( *conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) )
5595
  {
5596
    selectQmlQuery = QString( "SELECT styleName, styleQML"
5597
                              " FROM layer_styles"
5598
                              " WHERE f_table_catalog=%1"
5599
                              " AND f_table_schema=%2"
5600
                              " AND f_table_name=%3"
5601
                              " AND f_geometry_column %4"
5602
                              " ORDER BY CASE WHEN useAsDefault THEN 1 ELSE 2 END"
5603
                              ",update_time DESC LIMIT 1" )
5604
                       .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5605
                       .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5606
                       .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5607
                       .arg( geomColumnExpr );
5608
  }
5609
  else
5610
  {
5611
    selectQmlQuery = QString( "SELECT styleName, styleQML"
5612
                              " FROM layer_styles"
5613
                              " WHERE f_table_catalog=%1"
5614
                              " AND f_table_schema=%2"
5615
                              " AND f_table_name=%3"
5616
                              " AND f_geometry_column %4"
5617
                              " AND (type=%5 OR type IS NULL)"
5618
                              " ORDER BY CASE WHEN useAsDefault THEN 1 ELSE 2 END"
5619
                              ",update_time DESC LIMIT 1" )
5620
                       .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5621
                       .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5622
                       .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5623
                       .arg( geomColumnExpr )
5624
                       .arg( wkbTypeString );
5625
  }
5626

    
5627
  QgsPostgresResult result( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), selectQmlQuery ) );
5628

    
5629
  styleName = result.PQntuples() == 1 ? result.PQgetvalue( 0, 0 ) : QString();
5630
  QString style = result.PQntuples() == 1 ? result.PQgetvalue( 0, 1 ) : QString();
5631
  conn->unref();
5632

    
5633
  QgsPostgresUtils::restoreInvalidXmlChars( style );
5634

    
5635
  return style;
5636
}
5637

    
5638
int QgsPostgresProviderMetadata::listStyles( const QString &uri, QStringList &ids, QStringList &names, QStringList &descriptions, QString &errCause )
5639
{
5640
  errCause.clear();
5641
  QgsDataSourceUri dsUri( uri );
5642

    
5643
  QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, true );
5644
  if ( !conn )
5645
  {
5646
    errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() );
5647
    return -1;
5648
  }
5649

    
5650
  if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) )
5651
  {
5652
    return -1;
5653
  }
5654

    
5655
  if ( dsUri.database().isEmpty() ) // typically when a service file is used
5656
  {
5657
    dsUri.setDatabase( conn->currentDatabase() );
5658
  }
5659

    
5660
  QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) );
5661

    
5662
  QString selectRelatedQuery = QString( "SELECT id,styleName,description"
5663
                                        " FROM layer_styles"
5664
                                        " WHERE f_table_catalog=%1"
5665
                                        " AND f_table_schema=%2"
5666
                                        " AND f_table_name=%3"
5667
                                        " AND %4"
5668
                                        " AND (type=%5 OR type IS NULL)"
5669
                                        " ORDER BY useasdefault DESC, update_time DESC" )
5670
                                 .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5671
                                 .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5672
                                 .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5673
                                 .arg( dsUri.geometryColumn().isEmpty() ? "f_geometry_column is NULL" : QString( "f_geometry_column=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
5674
                                 .arg( wkbTypeString );
5675

    
5676
  QgsPostgresResult result( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), selectRelatedQuery ) );
5677
  if ( result.PQresultStatus() != PGRES_TUPLES_OK )
5678
  {
5679
    QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectRelatedQuery ) );
5680
    errCause = QObject::tr( "Error executing the select query for related styles. The query was logged" );
5681
    conn->unref();
5682
    return -1;
5683
  }
5684

    
5685
  int numberOfRelatedStyles = result.PQntuples();
5686
  for ( int i = 0; i < numberOfRelatedStyles; i++ )
5687
  {
5688
    ids.append( result.PQgetvalue( i, 0 ) );
5689
    names.append( result.PQgetvalue( i, 1 ) );
5690
    descriptions.append( result.PQgetvalue( i, 2 ) );
5691
  }
5692

    
5693
  QString selectOthersQuery = QString( "SELECT id,styleName,description"
5694
                                       " FROM layer_styles"
5695
                                       " WHERE NOT (f_table_catalog=%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column %4 AND type=%5)"
5696
                                       " ORDER BY update_time DESC" )
5697
                                .arg( QgsPostgresConn::quotedValue( dsUri.database() ) )
5698
                                .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) )
5699
                                .arg( QgsPostgresConn::quotedValue( dsUri.table() ) )
5700
                                .arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) )
5701
                                .arg( wkbTypeString );
5702

    
5703
  result = conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), selectOthersQuery );
5704
  if ( result.PQresultStatus() != PGRES_TUPLES_OK )
5705
  {
5706
    QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectOthersQuery ) );
5707
    errCause = QObject::tr( "Error executing the select query for unrelated styles. The query was logged" );
5708
    conn->unref();
5709
    return -1;
5710
  }
5711

    
5712
  for ( int i = 0; i < result.PQntuples(); i++ )
5713
  {
5714
    ids.append( result.PQgetvalue( i, 0 ) );
5715
    names.append( result.PQgetvalue( i, 1 ) );
5716
    descriptions.append( result.PQgetvalue( i, 2 ) );
5717
  }
5718

    
5719
  conn->unref();
5720

    
5721
  return numberOfRelatedStyles;
5722
}
5723

    
5724
bool QgsPostgresProviderMetadata::deleteStyleById( const QString &uri, const QString &styleId, QString &errCause )
5725
{
5726
  QgsDataSourceUri dsUri( uri );
5727
  bool deleted;
5728

    
5729
  QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, false );
5730
  if ( !conn )
5731
  {
5732
    errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() );
5733
    deleted = false;
5734
  }
5735
  else
5736
  {
5737
    QString deleteStyleQuery = QStringLiteral( "DELETE FROM layer_styles WHERE id=%1" ).arg( QgsPostgresConn::quotedValue( styleId ) );
5738
    QgsPostgresResult result( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), deleteStyleQuery ) );
5739
    if ( result.PQresultStatus() != PGRES_COMMAND_OK )
5740
    {
5741
      QgsDebugError(
5742
        QString( "PQexec of this query returning != PGRES_COMMAND_OK (%1 != expected %2): %3" )
5743
          .arg( result.PQresultStatus() )
5744
          .arg( PGRES_COMMAND_OK )
5745
          .arg( deleteStyleQuery )
5746
      );
5747
      QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( deleteStyleQuery ) );
5748
      errCause = QObject::tr( "Error executing the delete query. The query was logged" );
5749
      deleted = false;
5750
    }
5751
    else
5752
    {
5753
      deleted = true;
5754
    }
5755
    conn->unref();
5756
  }
5757
  return deleted;
5758
}
5759

    
5760
QString QgsPostgresProviderMetadata::getStyleById( const QString &uri, const QString &styleId, QString &errCause )
5761
{
5762
  QgsDataSourceUri dsUri( uri );
5763

    
5764
  QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri, true );
5765
  if ( !conn )
5766
  {
5767
    errCause = QObject::tr( "Connection to database failed using username: %1" ).arg( dsUri.username() );
5768
    return QString();
5769
  }
5770

    
5771
  QString style;
5772
  QString selectQmlQuery = QStringLiteral( "SELECT styleQml FROM layer_styles WHERE id=%1" ).arg( QgsPostgresConn::quotedValue( styleId ) );
5773
  QgsPostgresResult result( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), selectQmlQuery ) );
5774
  if ( result.PQresultStatus() == PGRES_TUPLES_OK )
5775
  {
5776
    if ( result.PQntuples() == 1 )
5777
      style = result.PQgetvalue( 0, 0 );
5778
    else
5779
      errCause = QObject::tr( "Consistency error in table '%1'. Style id should be unique" ).arg( QLatin1String( "layer_styles" ) );
5780
  }
5781
  else
5782
  {
5783
    QgsMessageLog::logMessage( QObject::tr( "Error executing query: %1" ).arg( selectQmlQuery ) );
5784
    errCause = QObject::tr( "Error executing the select query. The query was logged" );
5785
  }
5786

    
5787
  conn->unref();
5788

    
5789
  QgsPostgresUtils::restoreInvalidXmlChars( style );
5790

    
5791
  return style;
5792
}
5793

    
5794
QgsTransaction *QgsPostgresProviderMetadata::createTransaction( const QString &connString )
5795
{
5796
  return new QgsPostgresTransaction( connString );
5797
}
5798

    
5799
QMap<QString, QgsAbstractProviderConnection *> QgsPostgresProviderMetadata::connections( bool cached )
5800
{
5801
  return connectionsProtected<QgsPostgresProviderConnection, QgsPostgresConn>( cached );
5802
}
5803

    
5804
QgsAbstractProviderConnection *QgsPostgresProviderMetadata::createConnection( const QString &uri, const QVariantMap &configuration )
5805
{
5806
  return new QgsPostgresProviderConnection( uri, configuration );
5807
}
5808

    
5809
void QgsPostgresProviderMetadata::deleteConnection( const QString &name )
5810
{
5811
  deleteConnectionProtected<QgsPostgresProviderConnection>( name );
5812
}
5813

    
5814
void QgsPostgresProviderMetadata::saveConnection( const QgsAbstractProviderConnection *conn, const QString &name )
5815
{
5816
  saveConnectionProtected( conn, name );
5817
}
5818

    
5819
QgsAbstractProviderConnection *QgsPostgresProviderMetadata::createConnection( const QString &name )
5820
{
5821
  return new QgsPostgresProviderConnection( name );
5822
}
5823

    
5824

    
5825
QgsPostgresProjectStorage *gPgProjectStorage = nullptr;               // when not null it is owned by QgsApplication::projectStorageRegistry()
5826
QgsPostgresLayerMetadataProvider *gPgLayerMetadataProvider = nullptr; // when not null it is owned by QgsApplication::layerMetadataProviderRegistry()
5827

    
5828
void QgsPostgresProviderMetadata::initProvider()
5829
{
5830
  Q_ASSERT( !gPgProjectStorage );
5831
  gPgProjectStorage = new QgsPostgresProjectStorage;
5832
  QgsApplication::projectStorageRegistry()->registerProjectStorage( gPgProjectStorage ); // takes ownership
5833
  Q_ASSERT( !gPgLayerMetadataProvider );
5834
  gPgLayerMetadataProvider = new QgsPostgresLayerMetadataProvider();
5835
  QgsApplication::layerMetadataProviderRegistry()->registerLayerMetadataProvider( gPgLayerMetadataProvider ); // takes ownership
5836
}
5837

    
5838
void QgsPostgresProviderMetadata::cleanupProvider()
5839
{
5840
  QgsApplication::projectStorageRegistry()->unregisterProjectStorage( gPgProjectStorage ); // destroys the object
5841
  gPgProjectStorage = nullptr;
5842
  QgsApplication::layerMetadataProviderRegistry()->unregisterLayerMetadataProvider( gPgLayerMetadataProvider );
5843
  gPgLayerMetadataProvider = nullptr;
5844

    
5845
  QgsPostgresConnPool::cleanupInstance();
5846
}
5847

    
5848

    
5849
// ----------
5850

    
5851
void QgsPostgresSharedData::addFeaturesCounted( long long diff )
5852
{
5853
  QMutexLocker locker( &mMutex );
5854

    
5855
  if ( mFeaturesCounted >= 0 )
5856
    mFeaturesCounted += diff;
5857
}
5858

    
5859
void QgsPostgresSharedData::ensureFeaturesCountedAtLeast( long long fetched )
5860
{
5861
  QMutexLocker locker( &mMutex );
5862

    
5863
  /* only updates the feature count if it was already once.
5864
   * Otherwise, this would lead to false feature count if
5865
   * an existing project is open at a restrictive extent.
5866
   */
5867
  if ( mFeaturesCounted > 0 && mFeaturesCounted < fetched )
5868
  {
5869
    QgsDebugMsgLevel( QStringLiteral( "feature count adjusted from %1 to %2" ).arg( mFeaturesCounted ).arg( fetched ), 2 );
5870
    mFeaturesCounted = fetched;
5871
  }
5872
}
5873

    
5874
long long QgsPostgresSharedData::featuresCounted()
5875
{
5876
  QMutexLocker locker( &mMutex );
5877
  return mFeaturesCounted;
5878
}
5879

    
5880
void QgsPostgresSharedData::setFeaturesCounted( long long count )
5881
{
5882
  QMutexLocker locker( &mMutex );
5883
  mFeaturesCounted = count;
5884
}
5885

    
5886

    
5887
QgsFeatureId QgsPostgresSharedData::lookupFid( const QVariantList &v )
5888
{
5889
  QMutexLocker locker( &mMutex );
5890

    
5891
  QMap<QVariantList, QgsFeatureId>::const_iterator it = mKeyToFid.constFind( v );
5892

    
5893
  if ( it != mKeyToFid.constEnd() )
5894
  {
5895
    return it.value();
5896
  }
5897

    
5898
  mFidToKey.insert( ++mFidCounter, v );
5899
  mKeyToFid.insert( v, mFidCounter );
5900

    
5901
  return mFidCounter;
5902
}
5903

    
5904

    
5905
QVariantList QgsPostgresSharedData::removeFid( QgsFeatureId fid )
5906
{
5907
  QMutexLocker locker( &mMutex );
5908

    
5909
  QVariantList v = mFidToKey[fid];
5910
  mFidToKey.remove( fid );
5911
  mKeyToFid.remove( v );
5912
  return v;
5913
}
5914

    
5915
void QgsPostgresSharedData::insertFid( QgsFeatureId fid, const QVariantList &k )
5916
{
5917
  QMutexLocker locker( &mMutex );
5918

    
5919
  mFidToKey.insert( fid, k );
5920
  mKeyToFid.insert( k, fid );
5921
}
5922

    
5923
QVariantList QgsPostgresSharedData::lookupKey( QgsFeatureId featureId )
5924
{
5925
  QMutexLocker locker( &mMutex );
5926

    
5927
  QMap<QgsFeatureId, QVariantList>::const_iterator it = mFidToKey.constFind( featureId );
5928
  if ( it != mFidToKey.constEnd() )
5929
    return it.value();
5930
  return QVariantList();
5931
}
5932

    
5933
void QgsPostgresSharedData::clear()
5934
{
5935
  QMutexLocker locker( &mMutex );
5936
  mFidToKey.clear();
5937
  mKeyToFid.clear();
5938
  mFeaturesCounted = -1;
5939
  mFidCounter = 0;
5940
}
5941

    
5942
void QgsPostgresSharedData::clearSupportsEnumValuesCache()
5943
{
5944
  QMutexLocker locker( &mMutex );
5945
  mFieldSupportsEnumValues.clear();
5946
}
5947

    
5948
bool QgsPostgresSharedData::fieldSupportsEnumValuesIsSet( int index )
5949
{
5950
  QMutexLocker locker( &mMutex );
5951
  return mFieldSupportsEnumValues.contains( index );
5952
}
5953

    
5954
bool QgsPostgresSharedData::fieldSupportsEnumValues( int index )
5955
{
5956
  QMutexLocker locker( &mMutex );
5957
  return mFieldSupportsEnumValues.contains( index ) && mFieldSupportsEnumValues[index];
5958
}
5959

    
5960
void QgsPostgresSharedData::setFieldSupportsEnumValues( int index, bool isSupported )
5961
{
5962
  QMutexLocker locker( &mMutex );
5963
  mFieldSupportsEnumValues[index] = isSupported;
5964
}
5965

    
5966

    
5967
QgsPostgresProviderMetadata::QgsPostgresProviderMetadata()
5968
  : QgsProviderMetadata( QgsPostgresProvider::POSTGRES_KEY, QgsPostgresProvider::POSTGRES_DESCRIPTION )
5969
{
5970
}
5971

    
5972
QIcon QgsPostgresProviderMetadata::icon() const
5973
{
5974
  return QgsApplication::getThemeIcon( QStringLiteral( "mIconPostgis.svg" ) );
5975
}
5976

    
5977
#ifndef HAVE_STATIC_PROVIDERS
5978
QGISEXTERN QgsProviderMetadata *providerMetadataFactory()
5979
{
5980
  return new QgsPostgresProviderMetadata();
5981
}
5982
#endif
5983

    
5984

    
5985
QVariantMap QgsPostgresProviderMetadata::decodeUri( const QString &uri ) const
5986
{
5987
  const QgsDataSourceUri dsUri { uri };
5988
  QVariantMap uriParts;
5989

    
5990
  if ( !dsUri.database().isEmpty() )
5991
    uriParts[QStringLiteral( "dbname" )] = dsUri.database();
5992
  if ( !dsUri.host().isEmpty() )
5993
    uriParts[QStringLiteral( "host" )] = dsUri.host();
5994
  if ( !dsUri.port().isEmpty() )
5995
    uriParts[QStringLiteral( "port" )] = dsUri.port();
5996
  if ( !dsUri.service().isEmpty() )
5997
    uriParts[QStringLiteral( "service" )] = dsUri.service();
5998
  if ( !dsUri.username().isEmpty() )
5999
    uriParts[QStringLiteral( "username" )] = dsUri.username();
6000
  if ( !dsUri.password().isEmpty() )
6001
    uriParts[QStringLiteral( "password" )] = dsUri.password();
6002
  if ( !dsUri.authConfigId().isEmpty() )
6003
    uriParts[QStringLiteral( "authcfg" )] = dsUri.authConfigId();
6004
  if ( dsUri.wkbType() != Qgis::WkbType::Unknown )
6005
    uriParts[QStringLiteral( "type" )] = static_cast<quint32>( dsUri.wkbType() );
6006

    
6007
  if ( uri.contains( QStringLiteral( "selectatid=" ), Qt::CaseSensitivity::CaseInsensitive ) )
6008
    uriParts[QStringLiteral( "selectatid" )] = !dsUri.selectAtIdDisabled();
6009

    
6010
  if ( !dsUri.table().isEmpty() )
6011
    uriParts[QStringLiteral( "table" )] = dsUri.table();
6012
  if ( !dsUri.schema().isEmpty() )
6013
    uriParts[QStringLiteral( "schema" )] = dsUri.schema();
6014
  if ( !dsUri.keyColumn().isEmpty() )
6015
    uriParts[QStringLiteral( "key" )] = dsUri.keyColumn();
6016
  if ( !dsUri.srid().isEmpty() )
6017
    uriParts[QStringLiteral( "srid" )] = dsUri.srid();
6018

    
6019
  if ( uri.contains( QStringLiteral( "estimatedmetadata=" ), Qt::CaseSensitivity::CaseInsensitive ) )
6020
    uriParts[QStringLiteral( "estimatedmetadata" )] = dsUri.useEstimatedMetadata();
6021

    
6022
  if ( uri.contains( QStringLiteral( "sslmode=" ), Qt::CaseSensitivity::CaseInsensitive ) )
6023
    uriParts[QStringLiteral( "sslmode" )] = dsUri.sslMode();
6024

    
6025
  if ( !dsUri.sql().isEmpty() )
6026
    uriParts[QStringLiteral( "sql" )] = dsUri.sql();
6027
  if ( !dsUri.geometryColumn().isEmpty() )
6028
    uriParts[QStringLiteral( "geometrycolumn" )] = dsUri.geometryColumn();
6029

    
6030
  return uriParts;
6031
}
6032

    
6033

    
6034
QString QgsPostgresProviderMetadata::encodeUri( const QVariantMap &parts ) const
6035
{
6036
  QgsDataSourceUri dsUri;
6037
  if ( parts.contains( QStringLiteral( "dbname" ) ) )
6038
    dsUri.setDatabase( parts.value( QStringLiteral( "dbname" ) ).toString() );
6039
  if ( parts.contains( QStringLiteral( "port" ) ) )
6040
    dsUri.setParam( QStringLiteral( "port" ), parts.value( QStringLiteral( "port" ) ).toString() );
6041
  if ( parts.contains( QStringLiteral( "host" ) ) )
6042
    dsUri.setParam( QStringLiteral( "host" ), parts.value( QStringLiteral( "host" ) ).toString() );
6043
  if ( parts.contains( QStringLiteral( "service" ) ) )
6044
    dsUri.setParam( QStringLiteral( "service" ), parts.value( QStringLiteral( "service" ) ).toString() );
6045
  if ( parts.contains( QStringLiteral( "username" ) ) )
6046
    dsUri.setUsername( parts.value( QStringLiteral( "username" ) ).toString() );
6047
  if ( parts.contains( QStringLiteral( "password" ) ) )
6048
    dsUri.setPassword( parts.value( QStringLiteral( "password" ) ).toString() );
6049
  if ( parts.contains( QStringLiteral( "authcfg" ) ) )
6050
    dsUri.setAuthConfigId( parts.value( QStringLiteral( "authcfg" ) ).toString() );
6051
  if ( parts.contains( QStringLiteral( "type" ) ) )
6052
    dsUri.setParam( QStringLiteral( "type" ), QgsWkbTypes::displayString( static_cast<Qgis::WkbType>( parts.value( QStringLiteral( "type" ) ).toInt() ) ) );
6053
  if ( parts.contains( QStringLiteral( "selectatid" ) ) )
6054
    dsUri.setParam( QStringLiteral( "selectatid" ), parts.value( QStringLiteral( "selectatid" ) ).toString() );
6055
  if ( parts.contains( QStringLiteral( "table" ) ) )
6056
    dsUri.setTable( parts.value( QStringLiteral( "table" ) ).toString() );
6057
  if ( parts.contains( QStringLiteral( "schema" ) ) )
6058
    dsUri.setSchema( parts.value( QStringLiteral( "schema" ) ).toString() );
6059
  if ( parts.contains( QStringLiteral( "key" ) ) )
6060
    dsUri.setParam( QStringLiteral( "key" ), parts.value( QStringLiteral( "key" ) ).toString() );
6061
  if ( parts.contains( QStringLiteral( "srid" ) ) )
6062
    dsUri.setSrid( parts.value( QStringLiteral( "srid" ) ).toString() );
6063
  if ( parts.contains( QStringLiteral( "estimatedmetadata" ) ) )
6064
    dsUri.setParam( QStringLiteral( "estimatedmetadata" ), parts.value( QStringLiteral( "estimatedmetadata" ) ).toString() );
6065
  if ( parts.contains( QStringLiteral( "sslmode" ) ) )
6066
    dsUri.setParam( QStringLiteral( "sslmode" ), QgsDataSourceUri::encodeSslMode( static_cast<QgsDataSourceUri::SslMode>( parts.value( QStringLiteral( "sslmode" ) ).toInt() ) ) );
6067
  if ( parts.contains( QStringLiteral( "sql" ) ) )
6068
    dsUri.setSql( parts.value( QStringLiteral( "sql" ) ).toString() );
6069
  if ( parts.contains( QStringLiteral( "checkPrimaryKeyUnicity" ) ) )
6070
    dsUri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), parts.value( QStringLiteral( "checkPrimaryKeyUnicity" ) ).toString() );
6071
  if ( parts.contains( QStringLiteral( "geometrycolumn" ) ) )
6072
    dsUri.setGeometryColumn( parts.value( QStringLiteral( "geometrycolumn" ) ).toString() );
6073
  return dsUri.uri( false );
6074
}
6075

    
6076
QList<Qgis::LayerType> QgsPostgresProviderMetadata::supportedLayerTypes() const
6077
{
6078
  return { Qgis::LayerType::Vector };
6079
}
6080

    
6081
bool QgsPostgresProviderMetadata::saveLayerMetadata( const QString &uri, const QgsLayerMetadata &metadata, QString &errorMessage )
6082
{
6083
  return QgsPostgresProviderMetadataUtils::saveLayerMetadata( Qgis::LayerType::Vector, uri, metadata, errorMessage );
6084
}
6085

    
6086

    
6087
QgsProviderMetadata::ProviderCapabilities QgsPostgresProviderMetadata::providerCapabilities() const
6088
{
6089
  return QgsProviderMetadata::ProviderCapability::SaveLayerMetadata | QgsProviderMetadata::ProviderCapability::ParallelCreateProvider;
6090
}